Spring Security の認証処理アーキテクチャを学ぶ

Spring Security は割となんとなくで動作してくれるイメージだけど、カスタマイズしようとするとハマったりするので、アーキテクチャを知れば何とか分かるようになるのではないかという発想から調べてまとめてみた。

以下のようなフォーム認証を行うシナリオに沿って、どのようなクラスがどのような役割を担っているのかを調べる。

1. 未認証の状態で認証が必要なページへアクセスする
2. ログイン画面にリダイレクトされる
3. 情報を入力しログイン処理を実行する
4. ログインが完了し 1でアクセスしていた画面にリダイレクトされる

なお、バージョンは 5.2.1.RELEASE で確認している。

# ログイン画面へリダイレクトされるまで

認証が必要なページに対してまだ認証されていないユーザがアクセスした場合、ログイン画面へ飛ばされる仕組みについて。
ざっくりとしたフローは以下の通り。なお、今回はアクセスした際のチェック処理は説明しないが `AuthenticationException` がスローされるところからスタートする。

![ログイン遷移.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133607/1f166d76-8405-c44f-f94e-f2517cf9a954.png)


## 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 となるまでの流れ。ざっくりと以下のようになっている。

![認証.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133607/4d6b1402-3cab-1ee1-c205-e383b8dc1e2a.png)


## 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` の実装クラスは自分で作ることが多い。

# 認証情報の永続化

認証が完了して取得できた認証情報を永続化しておくことによって、認証済みであるかどうかを判定できるようになるし、ユーザ情報にもアクセスすることができる。

![永続化.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133607/c99d46cf-a611-d68b-f9a2-085481720f57.png)


## 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

![認証成功.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133607/a3ea60d4-a34d-0d35-1445-36acc06c15a1.png)

認証成功時の処理を定義する。
いくつか実装クラスが提供されているが、`SavedRequestAwareAuthenticationSuccessHandler` がデフォルトで利用される。
このクラスは `RequestCache` にリクエストが保持されている場合は、そのリクエストをリダイレクトする。
保持されていない場合は、設定されたデフォルトの URL にリダイレクトする。

未認証状態で認証が必要なページにアクセスし、認証画面にリダイレクトされる。
その後認証を完了させると、最初に表示しようとしていた画面へ遷移することができるのは、このクラスを利用しているため。

## AuthenticationFailureHandler

![認証失敗.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133607/34f7260e-f405-f8f3-48d3-cf19f88b3343.png)

認証失敗時の処理を定義する。
これもいくつか実装クラスが提供されているが、`SimpleUrlAuthenticationFailuerHandler` がデフォルトで利用される。
このクラスはいたってシンプルで、指定された URL に対してリダイレクトを行う。
また、認証失敗時に発生した例外を `HttpSession` に格納しておくことによって、リダイレクト先で参照が可能になる。

## 認証成功、失敗後の動作まとめ

認証成功、失敗後の動作を定義するにはぞれぞれ `AuthenticationSuccessHandler`、`AuthenticationFailuerHandler` を利用する。
提供されている実装クラスは指定された URL にリダイレクト（設定によってはフォワード）するだけだったりするので、高度なことをやりたいのであれば、実装クラスを作る必要がある。
また、認証方法によっては設定できないこともあるので注意が必要。

# ログアウト

ついでにログアウトも。
![ログアウト.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133607/14e7ada2-b6fe-7d69-6584-301ca62493e2.png)


## 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 なんかわかってきた気がする！（気のせい）