第2回：Spring Security 5でサポートされるOAuth 2.0 Loginの処理の流れを(大まかに)理解する

前回「[Spring Security 5でサポートされるOAuth 2.0 LoginをSpring Bootで使ってみる](https://qiita.com/kazuki43zoo/items/658eabbf0d6a60d3ed89)」で作成したデモアプリケーションをもとに、どのような流れでSpring Security 5がOAuth 2.0 Loginを実現しているか見ていきたいと思います。（今回はServlet Filterの粒度での処理の流れにとどめます→各Servlet Filterの中でどのようなクラスが使われて処理が行われているか？という話は乞うご期待！！）

# 前提バージョン

* Spring Boot 2.0.0.M7
* Spring Security 5.0.0.RELEASE

# Spring Securityのデバッグログを有効化

Spring Securityの仕組みを調べる際は、Spring Secuirtyのデバッグログを有効化しておくと、Spring SecurityのSecurity Filter達の動きを把握しやすくなります。

```properties:src/main/resources/application.properties
logging.level.org.springframework.security=debug
```

# Servlet Filterの適用状況

デモアプリケーションを起動すると、以下のようなログが出力され、すべてのリクエスト(`/*`)に対してSpring SecurityのServlet Filter(springSecurityFilterChain)が適用されていることが確認できます。

```diff
2017-11-23 21:40:39.451  INFO 66053 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2017-11-23 21:40:39.451  INFO 66053 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2017-11-23 21:40:39.451  INFO 66053 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2017-11-23 21:40:39.451  INFO 66053 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
+2017-11-23 21:40:39.452  INFO 66053 --- [ost-startStop-1] .s.DelegatingFilterProxyRegistrationBean : Mapping filter: 'springSecurityFilterChain' to: [/*]
```

# FilterChainProxy(springSecurityFilterChain)の適用状況

Spring Securityのデバッグログを有効にした状態でデモアプリケーションを起動すると、以下のようなログが出力され、パスパターン毎にSpring SecurityのSecurity Filter(実体はServlet Filter)の適用状況を確認することができます。（可読性のために改行していますが、実際は１行で出力されます）

```
2017-11-23 21:40:40.691  INFO 66053 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1,
 [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@3cae7b8b,
 org.springframework.security.web.context.SecurityContextPersistenceFilter@20e6c4dc,
 org.springframework.security.web.header.HeaderWriterFilter@327c7bea,
 org.springframework.security.web.csrf.CsrfFilter@5246a3b3,
 org.springframework.security.web.authentication.logout.LogoutFilter@151db587,
 org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter@26f7cdf8,
 org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter@681adc8f,
 org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4682eba5,
 org.springframework.security.web.savedrequest.RequestCacheAwareFilter@4d2a1da3,
 org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@68c87fc3,
 org.springframework.security.web.authentication.AnonymousAuthenticationFilter@184dbacc, 
 org.springframework.security.web.session.SessionManagementFilter@6c65860d,
 org.springframework.security.web.access.ExceptionTranslationFilter@3f672204,
 org.springframework.security.web.access.intercept.FilterSecurityInterceptor@18eec010]
```

# Security Filterの概要

本エントリ(OAuth 2.0 Login)に関係するSecurity Filter(Servlet Filter)の役割を簡単に紹介しておきましょう。

| Security Filter | 説明 |
| ----- | --- |
| `SecurityContextPersistenceFilter` | `SecurityContext`(認証情報を保持する領域)をリクエストを跨いで共有するための仕組みを提供するServlet Filter。デフォルト動作では、リクエスト間での共有のために`HttpSession`を利用する。 |
| `OAuth2AuthorizationRequestRedirectFilter` | OAuth 2.0(OpenID Connect 1.0)プロバイダの認可エンドポイント(リソースオーナのユーザ情報へのアクセス許可を得るためのエンドポイント)へリダイレクトするためのエンドポイントを提供するServlet Filter。デフォルト動作では、「`/oauth2/authorization/{registrationId}`」がエンドポイントパスになる。
| `OAuth2LoginAuthenticationFilter` | OAuth 2.0(OpenID Connect 1.0)のトークンエンドポイント（アクセストークンを取得するためのエンドポイント）を利用してデモアプリにログインするためのエンドポイント（プロバイダ側で認可を行った後にデモアプリ側に戻ってくる時に使われるエンドポイント）を提供するServlet Filter。デフォルト動作では、「`/login/oauth2/code/{registrationId}`」がエンドポイントパスになる。
| `DefaultLoginPageGeneratingFilter` | デフォルトのログインページを生成するためのエンドポイントを提供するServlet Filter。デフォルト動作では、「`GET /login`」がエンドポイントパスになる。ログインページを指定している場合は、本Servlet Filterは適用されません。
| `ExceptionTranslationFilter` | 認可エラーをハンドリングしてエラーレスポンスを行うためのServlet Filter。デフォルト動作では、ログインが必要な状況ではログインページを表示するためのエンドポイントへリダイレクトし、ログイン済みの場合は「403 Forbidden」が応答される（`AccessDeniedHandler`をカスタマイズして任意のエラー応答にすることもできる）。
| `FilterSecurityInterceptor` | 指定したアクセスポリシーをもとに認可処理を行うServlet Filter。

> Note:
>
> 本エントリでは、Spring Secuirtyの基本的な仕組みの話は割愛しますので、 Spring Securityの基本的な仕組みを知りたい方は @opengl-8080 さんの「[Spring Security 使い方メモ シリーズ](https://qiita.com/opengl-8080/items/032ed0fa27a239bdc1cc)」がオススメです！！

# 「インデックス画面表示要求 -> ログイン画面表示」の流れを理解する

ここでは、ログインをしていない状態でセキュアなページ（「インデックス画面」）の表示要求を行った時の動作をみていきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/117313/43b5a6e0-4441-da94-80e7-876cdeef5830.png)

| 処理の流れ |
| --- |
| リソースオーナは、ユーザエージェントを介して「インデックス画面表示要求(`GET /`)」を行う。 |
| `FilterSecurityInterceptor`は、「インデックス画面表示要求」に対して認可処理を行う。デモアプリケーションでは、「インデックス画面の表示要求」に対して「認証済みであること」というアクセスポリシーを定義しているため、ログインしていないユーザエージェントからのリクエストは認可エラーになる。 |
| `ExceptionTranslationFilter`は、認可エラー(`AccessDeniedException`)をハンドリングしてログイン画面へリダイレクトする。なお、リダイレクトする前に、認可エラーになったリクエスト情報をキャッシュ（デフォルト実装は`HttpSession`に格納）しておくことで、ログイン成功後にインデックス画面を表示できるようにする。 |
| ユーザエージェントは、「ログイン画面表示要求(`GET /login`)」を行う。 |
| `DefaultLoginPageGeneratingFilter`は、プロバイダ（デモアプリではGitHub）経由でログインを行うためのログイン画面を応答する。 |

# 「ログイン要求(認可画面表示要求) -> 認可画面表示」の流れを理解する

ここでは、`DefaultLoginPageGeneratingFilter`によって生成されたログイン画面からプロバイダ提供の認可画面の表示要求を行った時の動作をみておきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/117313/88f20747-7f55-5943-b54b-594f1cf7d02a.png)

| 処理の流れ |
| --- |
| リソースオーナは、ログイン画面に表示されているリンクを押下して、「認可画面表示要求（`GET /oauth2/authorization/{registrationId}`）」を行う。デモアプリケーションではプロバイダにGitHubを利用しているので、`registrationId`は`github`になる。 |
| `OAuth2AuthorizationRequestRedirectFilter`は、クライアント登録情報(`ClientRegistration`)を参照し、プロバイダ提供の認可エンドポイントへリダイレクトする。 |
| GitHubは、リソースオーナの認証を行った後に認可画面を表示する。 |

# 「認可要求 -> インデックス画面表示」の流れを理解する

ここでは、GitHubの認可画面で認可要求を行った時の動作をみておきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/117313/52f98df6-6f4c-dbc5-e95b-1a63644194e7.png)


| 処理の流れ |
| --- |
| リソースオーナは、認可画面に表示されている「認可ボタン」を押下して、プロバイダのユーザ情報を使用してデモアプリケーションにログインすることを許可する。 |
| GitHubは、デモアプリケーションが指定した遷移先（認証処理を行うエンドポイントへアクセスするためのURL）に認可コードを付与してリダイレクトする。 |
| ユーザエージェントは、「認証処理要求（`GET /login/oauth2/code/{registrationId}`）」を行う。デモアプリケーションではプロバイダにGitHubを利用しているので、`registrationId`は`github`になる。 |
| `OAuth2LoginAuthenticationFilter`は、プロバイダ提供のトークンエンドポイントにリクエストを行い、プロバイダから受け取った認可コードに対応するアクセストークンを取得する。 |
| `OAuth2LoginAuthenticationFilter`は、プロバイダから取得したアクセストークンを使用してユーザ情報エンドポイントにリクエストを行い、ソースオーナのユーザ情報を取得する。 |
| `OAuth2LoginAuthenticationFilter`は、トークンエンドポイント及びユーザ情報エンドポイントから取得を利用して認証情報を生成し、`SecurityContextHolder`に設定する。（ここで認証済み状態になる） |
| `OAuth2LoginAuthenticationFilter`は、認可エラー時にキャッシュしたリクエスト情報を参照して「インデックス画面表示要求（リダイレクト）」を行う。 |
| `SecurityContextPersistenceFilter`は、認証成功時に生成した認証情報を`SecurityContextHolder`から取り出して`HttpSession`に保存する。 |
| ユーザエージェントは、「インデックス画面表示要求（`GET /`）」を行う。 |
| `SecurityContextPersistenceFilter`は、`HttpSession`から認証情報を取得して`SecurityContextHolder`に設定する。この処理を行うことで、リクエストを跨いで認証情報を共有することができる（＝認証済み状態を保つことができる）。 |
| `FilterSecurityInterceptor`は、「インデックス画面表示要求」に対して認可処理を行う。この時点では認証済み状態になっているため、認可OKとなり後続処理（インデックス画面表示処理）が実行される。 |
| `DemoController`は、「インデックス画面表示要求」に対応する処理を行い、インデックス画面を応答する。 |

# まとめ

Spring Security 5提供のOAuth 2.0 Loginの仕組みは、Spring Security 4以前から提供していたSecurity Filter群に対して、`OAuth2AuthorizationRequestRedirectFilter`と`OAuth2LoginAuthenticationFilter`の2つのSecurity Filterを追加することで実現していることがわかったと思います。

`OAuth2AuthorizationRequestRedirectFilter`は完全にOAuth 2.0 Login独自のSecurity Filterですが、`OAuth2LoginAuthenticationFilter`はフォーム認証でいうところの`UsernamePasswordAuthenticationFilter`と同じ役割を担います。`UsernamePasswordAuthenticationFilter`はログイン画面で入力された「ユーザ名」と「パスワード」を使って認証処理を行うのに対し、`OAuth2LoginAuthenticationFilter`はプロバイダ（認可サーバ）が払い出した「認可コード」を使って認証処理を行うという点がこの２つのSecurity Filter(認証Filter)の違いになります。

次回は、`OAuth2AuthorizationRequestRedirectFilter`と`OAuth2LoginAuthenticationFilter`が、どのようなクラスを使用して処理を行っているかを紹介していきたいと思います。

# 参考サイト

* https://qiita.com/opengl-8080/items/032ed0fa27a239bdc1cc