Spring Security は割となんとなくで動作してくれるイメージだけど、カスタマイズしようとするとハマったりするので、アーキテクチャを知れば何とか分かるようになるのではないかという発想から調べてまとめてみた。
以下のようなフォーム認証を行うシナリオに沿って、どのようなクラスがどのような役割を担っているのかを調べる。
- 未認証の状態で認証が必要なページへアクセスする
- ログイン画面にリダイレクトされる
- 情報を入力しログイン処理を実行する
- ログインが完了し 1でアクセスしていた画面にリダイレクトされる
なお、バージョンは 5.2.1.RELEASE で確認している。
ログイン画面へリダイレクトされるまで
認証が必要なページに対してまだ認証されていないユーザがアクセスした場合、ログイン画面へ飛ばされる仕組みについて。
ざっくりとしたフローは以下の通り。なお、今回はアクセスした際のチェック処理は説明しないが AuthenticationException
がスローされるところからスタートする。
ExceptionTranslationFilter
発生した例外の原因例外を検査し、AuthenticationException
が含まれている場合に、リクエストの内容をRequestCache
に保存しつつ、AuthenticationEntryPoint
を利用して認証処理を開始させる。
RequestCache
は通常は HttpSession
を利用してリクエスト内容を保存する HttpSessionRequestCache
を利用する。
RequestCache
が何に利用されるのかは後ほど。
AuthenticationEntryPoint
認証処理を開始する。
例えば、LoginUrlAuthenticationEntryPoint
は設定されたログインページの URL に対してリダイレクトを行う。
BasicAuthenticationEntryPoint
は Basic 認証を開始するために、ステータスコードを 401 に、header に WWW-Authenticate
を付与したレスポンスを返す。
ログイン画面遷移まとめ
ExceptionTranslationFilter
によってハンドリングされるが、あまりカスタムすることもないので意識しなくてよさそう。
どのように認証を開始するかは AuthenticationEntryPoint
を適切に選択、設定する必要がある。
例えばログイン画面に遷移させるのであれば、LoginUrlAuthenticationEntryPoint
を利用してログイン画面の URL を設定する必要があるし、SSO のように事前に認証される前提で、認証処理を開始する必要がないならば Http403ForbiddenEntryPoint
を利用するなど。
DelegatingAuthenticationEntryPoint
を利用すればリクエストに対して利用する実装クラスを定義することもできるっぽい。
認証処理
まずはユーザが入力した情報を受け取り、認証 OK となるまでの流れ。ざっくりと以下のようになっている。
AuthenticationManager
認証をつかさどるのは AuthenticationManager
。
Authentication
オブジェクトを受け取り、認証 OK の場合は Authentication
オブジェクトを返す。
引数の Authentication
は認証に必要な principal と credentials のみが入っていて、戻り値の Authentication
には authorities や details などの情報も入っている。
ProviderManager
AuthenticationManager
の実装クラスとして ProviderManager
がある。デフォルトではこのクラスを利用する。
このクラスは複数の AuthenticationProvider
に対して認証処理を委譲し、1 つでも認証 OK となれば認証成功とする。
こうすることによって、例えばデータベースに格納された情報を利用した Form 認証と、LDAP による認証など、複数の認証処理を組み合わせることができる。
また、戻り値となる Authentication
から credentials を削除し、機密情報が永続化されることを防ぐことができる。
AuthenticationProvider
認証を行う。パスワードの検証が必要なのであればこのクラスで行う。
また、認証可能な Authentication
の実装クラスであるかどうかを判定するメソッドがある。
例えば、通常の Form 認証であれば、ユーザ名とパスワードがあれば認証処理を行うことができるが、すべての認証処理がそれで充分だとは限らない。なので、認証処理を行うことができるかどうかを supports(Class<?> authentication)
メソッドで判定する。
一般的な認証方法は実装クラスが提供されているので、基本的にそれらを利用すればいい。
ただし、ユーザの情報を取得してくる部分については、アプリケーションが利用しているデータベースなどの定義に依存するので、そこの部分のみ実装するというパターンが多い。
UserDetailsService
前述のユーザ情報の取得に関する部分の処理を行うためのインターフェース。
このインターフェースの実装クラスを作成してAuthenticationProvider
に設定する、といった使い方をすることが多い。
取得する情報は UserDetails
を実装したクラスでないといけない。
なお、JdbcDaoImpl
という実装クラスも提供されているが、取得できる情報が限られるため、自分で実装することになると思う。
認証処理呼び出す
基本的には Filter が呼び出す。例えば Form 認証を行う場合は UsernamePasswordAuthenticationFilter
が呼び出す。
この Filter は、ユーザからの入力情報などから Authenticaion
の実装クラスである UsernamePasswordAuthenticationToken
オブジェクトを作成し、AuthenticationManager
の認証処理を呼び出す。
認証が成功した場合、取得した認証情報を SecurityContextHolder
に設定するまでがお仕事。
(SecurityContextHolder
については後述)
認証処理ここまでのまとめ
AuthenticationManager
を実装することはなさそう。デフォルトの実装クラスである ProviderManager
を利用すればいいと思われる。
AuthenticationProvider
は概ね所望する実装クラスが提供されていると思うので、適切に選択する。ただし WebAuthn など、まだ Spring Security がサポートしていない新しい認証方式を作りたいのであれば実装する必要がありそう。Filter についても同様。
ユーザの情報を取得する UserDetailsService
の実装クラスは自分で作ることが多い。
認証情報の永続化
認証が完了して取得できた認証情報を永続化しておくことによって、認証済みであるかどうかを判定できるようになるし、ユーザ情報にもアクセスすることができる。
SecurityContext
認証情報(Authentication
)を保持するオブジェクト。
認証情報をそのまま永続化するのではなく、この SecurityContext
を永続化する。
SecurityContextRepository
SecurityContext
を永続化する処理を行うためのインターフェース。
認証情報を永続化する場所としては HttpSession
が思い浮かぶと思うが、Spring Security でも HttpSession
を利用する実装クラスである HttpSessionSecurityContextRepository
が提供されている。(デフォルトではそれを利用する)
SecurityContextHolder
SecurityContext
を保持するクラス。デフォルトでは ThreadLocal
を利用して保持する。
認証情報にアクセスする場合は、直接 HttpSession
にアクセスするのではなく、この SecurityContextHolder
から取り出すことができる。
SecurityContextPersistenceFilter
諸々の処理が始まる前に SecurityContextRepository
から SecurityContext
を取得し、SecurityContextHolder
に設定する。
諸々の処理が終わった後に SecurityContextHolder
から SecurityContext
を取得し、SecurityContextRepository
を利用して永続化を行い、SecurityContextHolder
から SecurityContext
を削除する。
SecurityContextHolder
が ThreadLocal
を利用しているが、この Filter が削除処理を行ってくれる。
永続化まとめ
認証情報は SecurityContext
として永続化され、SecurityContextRepository
によって永続化に関する処理が行われるが、あまり意識する必要はない。
SecurityContextHolder
から SecurityContext
を取得できるということを覚えておけばよさそう。
認証成功、失敗後の動作
認証処理を呼び出す Filter には認証成功、失敗時の動作を定義できるものもある。
例えば Form 認証を行う UsernamePasswordAuthenticationFilter
には定義可能だし、Basic 認証を行う BasicAuthenticationFilter
には定義できない。
AuthenticationSuccessHandler
認証成功時の処理を定義する。
いくつか実装クラスが提供されているが、SavedRequestAwareAuthenticationSuccessHandler
がデフォルトで利用される。
このクラスは RequestCache
にリクエストが保持されている場合は、そのリクエストをリダイレクトする。
保持されていない場合は、設定されたデフォルトの URL にリダイレクトする。
未認証状態で認証が必要なページにアクセスし、認証画面にリダイレクトされる。
その後認証を完了させると、最初に表示しようとしていた画面へ遷移することができるのは、このクラスを利用しているため。
AuthenticationFailureHandler
認証失敗時の処理を定義する。
これもいくつか実装クラスが提供されているが、SimpleUrlAuthenticationFailuerHandler
がデフォルトで利用される。
このクラスはいたってシンプルで、指定された URL に対してリダイレクトを行う。
また、認証失敗時に発生した例外を HttpSession
に格納しておくことによって、リダイレクト先で参照が可能になる。
認証成功、失敗後の動作まとめ
認証成功、失敗後の動作を定義するにはぞれぞれ AuthenticationSuccessHandler
、AuthenticationFailuerHandler
を利用する。
提供されている実装クラスは指定された URL にリダイレクト(設定によってはフォワード)するだけだったりするので、高度なことをやりたいのであれば、実装クラスを作る必要がある。
また、認証方法によっては設定できないこともあるので注意が必要。
ログアウト
LogoutFilter
ログアウト処理を行う LogoutHandler
を呼び出したあと、後処理を行う LogoutSuccessHandler
を呼び出す。
ログアウトを行う URL を定義できる。(デフォルトは /logout
)
LogoutHandler
ログアウト処理を行う。SecurityContextLogoutHandler
では HttpSession
を破棄している。
というか HttpSession
に認証情報が格納されている前提なのか・・・。
SecurityContextRepository
の実装次第では HttpSession
に格納されているとは限らないと思うけど。でも削除用のメソッドないしなー。うーむ・・・。
なお、LogoutFilter
は CompositeLogoutHandler
という複数の LogoutHanlder
をまとめるクラスを利用しているため、必要に応じて複数の LogoutHandler
を順番に実行できる。
LogoutSuccessHandler
ログアウト完了後の処理を行う。SimpleUrlLogoutSuccessHandler
では指定された URL にリダイレクトする。
ログアウトまとめ
ログアウト処理をカスタマイズしたいのであれば、LogoutHandler
を実装して LogoutFilter
に設定すればいい。
ログアウト完了後の画面遷移などをカスタマイズしたいのであれば、LogoutSuccessHandler
を実装して LogoutFilter
に設定すればいい。
参考URL
- https://docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/reference/htmlsingle/#overall-architecture
- https://www.slideshare.net/mobile/masatoshitada7/spring-security-meetup
- https://www.slideshare.net/mobile/RyosukeUchitate/formspring-security
さいごに
Spring Security なんかわかってきた気がする!(気のせい)