Spring Security の基本とOIDC認証時の動作

皆さん、こんにちは。技術開発グループのn-ozawanです。
寝違えました。首が痛いです。「寝違え 予防」と検索したら、「パソコン、スマホの使用時間を減らす」とありました。これも職業病なのかもしれません。

本題です。
Spring Security、難しいですよね。前回、KeycloakへのOIDC認証を行うコードを実装しました。その際、Spring Securityの動きについて簡単にしか触れていませんでしたので、今回はSpring Securityの動きを詳しく追っていきたいと思います。

Spring Security

Spring Securityはフィルタで動く

Spring Securityはフィルタで動作します。フィルタというのはサーブレットアプリの1つの機能です。通常、サーブレットによるWebアプリを構築する場合、業務ロジックをサーブレット(Servlet)に実装します。フィルタは、クライアントからリクエストを受け取り、サーブレットの前に行われる処理になります。

フィルタの主な使い道は、個別の業務ロジックを行う前に、各業務ロジックで共通的に行いたい処理を行うことです。特にセキュリティ対策などは、業務ロジック毎に実装するのではなく、共通的に処理した方が漏れがないため、フィルタで処理するのが適切と言えます。

上の図は公式ドキュメントから引用しました。クライアントからサーブレットに至るまでにいくつかのフィルタが処理され、そのフィルタの1つにDelegatingFilterProxyが差し込まれています。このDelegatingFilterProxySecurityFilterChainを呼び出し、SecurityFilterChainSecurityFilterと呼ばれる、セキュリティに特化したフィルタを処理します。

以下は、前回紹介した、OIDC認証の最小コードです。このコードで何をやっているのかというと、OIDC認証を行うためのSecurityFilterChainを生成しています。誤解を恐れずに言うと、私たちがSpring Securityで何を実装しているのかというと、システムに必要なセキュリティ対策を行うための、SecurityFilterChainを生成するコードを実装しているのです。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2Login(Customizer.withDefaults());

    return http.build();
  }
}

Security Filter って何があるの?

http.build()により、セキュリティ対策に必要なSecurity Filterで構成されたSecurityFilterChainが生成されます。何も問題が起こらなければ、どのSecurity Filterで構成されたのかは気にする必要もないのですが、いざ原因不明の問題が発生した場合はそうもいきません。どのSecurity Filterで構成されたのかを知りたいときは、Spring起動時にコンソールに出力された内容を見れば分かります。

赤枠で囲ったところに、どのSecurity Filterで構成されたのかが示されています。なんだか見る気力が減りそうなぐらい長いですが、「, (カンマ)」 で改行すれば少しは見やすくなります。一例として、http.build()のみでSecurityFilterChainを生成した場合、構成されるSecurity Filterは以下の通りとなります。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.build();
  }
}

// 2023-12-13T18:21:12.161+09:00  INFO 514188 --- [  restartedMain] o.s.s.web.DefaultSecurityFilterChain     : 
// Will secure any request with [
//   org.springframework.security.web.session.DisableEncodeUrlFilter@11b918a, 
//   org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@33be90bc, 
//   org.springframework.security.web.context.SecurityContextHolderFilter@77b7c830, 
//   org.springframework.security.web.header.HeaderWriterFilter@5f8a34e6, 
//   org.springframework.security.web.csrf.CsrfFilter@47969f67, 
//   org.springframework.security.web.authentication.logout.LogoutFilter@19895634, 
//   org.springframework.security.web.savedrequest.RequestCacheAwareFilter@4b3e9e3, 
//   org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4b016aa6, 
//   org.springframework.security.web.authentication.AnonymousAuthenticationFilter@13abf4c6, 
//   org.springframework.security.web.access.ExceptionTranslationFilter@e30e7e3
// ]

OIDC認証を行うようにhttp.build()をした場合は以下の通りです。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2Login(Customizer.withDefaults());

    return http.build();
  }
}

// 2024-01-15T10:30:06.325+09:00  INFO 13391 --- [  restartedMain] o.s.s.web.DefaultSecurityFilterChain     : 
// Will secure any request with [
//   org.springframework.security.web.session.DisableEncodeUrlFilter@14290b20, 
//   org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@18255ef0, 
//   org.springframework.security.web.context.SecurityContextHolderFilter@4aca3dea, 
//   org.springframework.security.web.header.HeaderWriterFilter@1dff441d, 
//   org.springframework.security.web.csrf.CsrfFilter@3bb97e9a, 
//   org.springframework.security.web.authentication.logout.LogoutFilter@b61f7a8, 
//   org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter@65a89391, 
//   org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter@4ab8d712, 
//   org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@6f53064d, 
//   org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@6cb75b20, 
//   org.springframework.security.web.savedrequest.RequestCacheAwareFilter@46c1e457, 
//   org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@58f6e8c7, 
//   org.springframework.security.web.authentication.AnonymousAuthenticationFilter@38cf2061, 
//   org.springframework.security.web.access.ExceptionTranslationFilter@59c59da2, 
//   org.springframework.security.web.access.intercept.AuthorizationFilter@e2fdf3f
// ]

両者を比べてみると、OIDC認所を行うSecurityFilterChainには、OAuth2AuthorizationRequestRedirectFilterOAuth2LoginAuthenticationFilterAuthorizationFilterなどが追加されています。詳細は後述しますが、これらはOIDC認証を行うのに必要なSecurity Filterになります。このことから、「SecurityFilterChainの生成を実装している」というのかなんとなくイメージ出来るかと思います。

OIDC認証時のSpring Securityの動きを追う

では、追加されたOAuth2AuthorizationRequestRedirectFilterOAuth2LoginAuthenticationFilterAuthorizationFilterにスポットを当てて、前回実装したOIDC認証の動きを追って行きましょう。

① OIDC認証の始まり

AuthorizationFilterは、SecurityFilterChainを生成する際に指定したauthorizeHttpRequests(...)メソッドの内容に則り、そのリクエストに対してユーザーが適切な権限を有しているかチェックします。前回のコードでは、authorize -> authorize.anyRequest().authenticated()でしたので、アクセスしてきたユーザーが認証済みかどうかをチェックしています。

まだ認証されていないユーザーがアクセスしてきた場合、AuthorizationFilterAccessDeniedException例外をthrowします。throwされた例外はExceptionTranslationFilterでcatchされます。ExceptionTranslationFilterは、リクエスト内容を一時保存(※一時保存したリクエストの使い道は後述)して、/oauth2/authorization/{registrationId}へリダイレクトすることで、OIDC認証の開始を行います。

② 認可エンドポイントへのリダイレクト

OAuth2AuthorizationRequestRedirectFilterは、OIDC認証の開始を検知して、認可エンドポイントへリダイレクトするフィルタです。Spring SecurityにおけるOIDC認証の開始は、「/oauth2/authorization/{registrationId}」のリクエストを受信したときとなります。

なお、OAuth2AuthorizationRequestRedirectFilterが処理された場合、後続のフィルタは呼ばれなくなります。

③ コールバックを受けての各種トークンを取得

OAuth2LoginAuthenticationFilterは、redirect-uriの受信を検知して、認可コードから各種トークンと、ユーザー情報を取得するフィルタです。正常に取得することが出来た場合、認証情報をセッションに格納します。最後にOIDC認証の開始時に一時保存したリクエストを取り出し、リダイレクトします。

④ 認証されているかチェック

最後に改めてAuthorizationFilterが、アクセスしてきたユーザーが認証済みかどうかをチェックします。これまでの工程でユーザーは正しく認証していますので、ページが問題なく表示されます。

まとめ

まとめとして、以下に全体のシーケンスを示します。

おわりに

Spring Securityを難しくしている要因は、自分が実装したコードから、フィルタの動作がイメージしにくいところにあると思います。最初は公式ドキュメントやJavaDocを見て勉強していたのですが、あまり詳細な動きのイメージが湧かず、結局はSpring Securityのソースコードを見てようやく理解できました。

今回はOIDC認証にスポットを当てて、Spring Securityについて解説しました。しかしSpring SecurityはOIDC認証以外にも、SAML2やX.509認証、ユーザー名/パスワード認証など、数多くの認証方法をサポートしています。その分、Security Filterの数も多く、今回説明した動きとはまた違った動きをすることでしょう。Spring Securityのすべてを理解するのは困難を極めますね。

ではまた。

Recommendおすすめブログ