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のすべてを理解するのは困難を極めますね。
ではまた。
