Spring Security の基本とOIDC認証時の動作
皆さん、こんにちは。技術開発グループのn-ozawanです。
寝違えました。首が痛いです。「寝違え 予防」と検索したら、「パソコン、スマホの使用時間を減らす」とありました。これも職業病なのかもしれません。
本題です。
Spring Security、難しいですよね。前回、KeycloakへのOIDC認証を行うコードを実装しました。その際、Spring Securityの動きについて簡単にしか触れていませんでしたので、今回はSpring Securityの動きを詳しく追っていきたいと思います。
目次
Spring Security
Spring Securityはフィルタで動く
Spring Securityはフィルタで動作します。フィルタというのはサーブレットアプリの1つの機能です。通常、サーブレットによるWebアプリを構築する場合、業務ロジックをサーブレット(Servlet)に実装します。フィルタは、クライアントからリクエストを受け取り、サーブレットの前に行われる処理になります。
フィルタの主な使い道は、個別の業務ロジックを行う前に、各業務ロジックで共通的に行いたい処理を行うことです。特にセキュリティ対策などは、業務ロジック毎に実装するのではなく、共通的に処理した方が漏れがないため、フィルタで処理するのが適切と言えます。

上の図は公式ドキュメントから引用しました。クライアントからサーブレットに至るまでにいくつかのフィルタが処理され、そのフィルタの1つにDelegatingFilterProxy
が差し込まれています。このDelegatingFilterProxy
がSecurityFilterChain
を呼び出し、SecurityFilterChain
はSecurityFilter
と呼ばれる、セキュリティに特化したフィルタを処理します。
以下は、前回紹介した、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
には、OAuth2AuthorizationRequestRedirectFilter
とOAuth2LoginAuthenticationFilter
、AuthorizationFilter
などが追加されています。詳細は後述しますが、これらはOIDC認証を行うのに必要なSecurity Filter
になります。このことから、「SecurityFilterChain
の生成を実装している」というのかなんとなくイメージ出来るかと思います。
OIDC認証時のSpring Securityの動きを追う
では、追加されたOAuth2AuthorizationRequestRedirectFilter
とOAuth2LoginAuthenticationFilter
、AuthorizationFilter
にスポットを当てて、前回実装したOIDC認証の動きを追って行きましょう。
① OIDC認証の始まり

AuthorizationFilter
は、SecurityFilterChain
を生成する際に指定したauthorizeHttpRequests(...)
メソッドの内容に則り、そのリクエストに対してユーザーが適切な権限を有しているかチェックします。前回のコードでは、authorize -> authorize.anyRequest().authenticated()
でしたので、アクセスしてきたユーザーが認証済みかどうかをチェックしています。
まだ認証されていないユーザーがアクセスしてきた場合、AuthorizationFilter
はAccessDeniedException
例外を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のすべてを理解するのは困難を極めますね。
ではまた。