「なんとなく」を卒業する。Filter構造から理解するSpring Securityの「現代的」な実装
はじめに
現在の案件の調査事項としてSpring Securityを触る機会がありました。
以前参加していたSpring Bootの案件でも利用しており、JavaでのWeb開発におけるデファクトスタンダード的な立ち位置だと感じています。
ただ、機能が強力なぶん「なんとなく動いているけど中身がよく分からない(コピペで済ませている)」「導入していること自体は知っているが、あまり中身を見たことがない」となりがちな部分でもあります。
備忘録も兼ねて、「Spring Securityがどうやってリクエストを捌いているのか(アーキテクチャ)」 と 「最新の基本的な設定方法」 について整理しました。
対象環境
この記事は以下のバージョンを前提としています。
- Java 17+
- Spring Boot 3.x
- Spring Security 6.x
Spring Securityとは
一言で言うと、Springアプリケーションのためのセキュリティフレームワークです。
主に以下の機能を提供します。
- 認証(Authentication): 「あなたは誰ですか?」を確認する(ログインなど)
- 認可(Authorization): 「あなたは何をしていいですか?」を制御する(管理者権限など)
- セキュリティ対策: CSRFやセッション固定攻撃などの対策を標準装備
最大の特徴は、Servlet Filterを中心とした構成になっており、Controllerにリクエストが届く前にセキュリティチェックが行われる点です。
アーキテクチャの全体像
Spring Securityを理解する上で一番重要なのが「リクエストの流れ」です。
リクエストの流れ
この記事で解説するのは、上図の SpringSecurity - SecurityFilterChain の部分 です。
リクエストが DispatcherServlet(Spring MVCのコントローラー処理)に到達する 「手前」 で、どのようなチェックが行われているのかを見ていきます。
-
Servlet FilterChain(この記事の主役)
Spring Securityの実体は、巨大なFilterの集まりです。リクエストが来ると、Spring Securityが登録した複数のFilterが順番に処理を行います(認証チェック、CSRFトークンの検証など)。 -
DispatcherServlet
全てのFilterを無事に通過して初めて、Spring MVCの処理(DispatcherServlet)が始まります。
これはSpring MVCのフロントコントローラーであり、リクエストされたURLをもとに「どのControllerのメソッドを呼び出すか」を決定し、振り分けを行います。 -
Controller以降
実際の業務ロジック(Controller)が実行されます。
つまり、「設定ミスでControllerまで到達できずに403エラーになる」 というのは、DispatcherServletにたどり着く手前、Filter群のどこかで引っかかっている状態と言えます。
Security Filter の中身(主要なもの)
Spring Securityは「特定の責務を持ったFilter」がたくさん連なってできています。これを SecurityFilterChain と呼びます。
トラブルシューティングでよく目にする主要なFilterを表にまとめました。これらは基本的に上から順に実行されます。
| Filter名 | 役割 | 主な機能 |
|---|---|---|
| SecurityContextHolderFilter | 認証情報の復元 | リクエスト開始時に、セッション(HttpSession)等から「ログイン済みかどうか」を復元する。 |
| CsrfFilter | CSRF対策 | POST等の更新系リクエストに正しいトークンが含まれているか検証する。 |
| LogoutFilter | ログアウト処理 |
/logout へのアクセスを検知して認証情報を破棄する。 |
| UsernamePasswordAuthenticationFilter | 認証処理 | フォームログイン時、送信されたID/PASSを受け取り検証処理を開始する。 |
| ExceptionTranslationFilter | 例外ハンドリング | 認証エラーならログイン画面へ、認可エラーなら403画面へ振り分ける「交通整理」を行う。 |
| AuthorizationFilter | 認可の執行 | 「このURLはこの権限が必要」というルールをチェックする(v6からの主役)。 |
実装例(SecurityFilterChain)
現在のSpring Security(Spring Boot 3.x系)では、WebSecurityConfigurerAdapter を継承するのではなく、SecurityFilterChain をBean登録するスタイル(Lambda DSL)が推奨されています。
以下は、一般的なWebアプリケーション(RDB利用・フォームログイン)を想定した設定クラスのサンプルです。
「デフォルトで安全(Secure by Default)」 の思想に基づき、余計な無効化設定を行わないシンプルな構成です。
- 静的リソースは全許可
- 管理者(ADMIN)と一般(USER)でパスを分ける
- フォームログイン・ログアウト設定
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
// アカウント登録時のパスワードエンコード等で利用するためにDI管理する
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// --- 1. 認可(アクセス権限)設定 ---
.authorizeHttpRequests(authz -> authz
// 静的リソース(CSS, JS, 画像など)は誰でもアクセスOK
// PathRequest.toStaticResources() は /css/**, /js/** などを自動マッピングします
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
// 認証なしでアクセスできるURL(ログイン画面、登録画面など)
.requestMatchers("/signup", "/login", "/error").permitAll()
// 権限によるアクセス制御("ROLE_" プレフィックスは自動付与される)
.requestMatchers("/members/user/**").hasRole("USER") // USER権限のみ
.requestMatchers("/members/admin/**").hasRole("ADMIN") // ADMIN権限のみ
// 上記以外はすべて認証が必要
.anyRequest().authenticated()
)
// --- 2. フォームログイン設定 ---
.formLogin(login -> login
// ログイン処理を行うURL(デフォルトは /login)
// .loginProcessingUrl("/login")
// ログイン成功時のリダイレクト先
.defaultSuccessUrl("/")
// ログイン失敗時のリダイレクト先
.failureUrl("/login?error")
// ログイン画面自体は誰でもアクセス可能にする
.permitAll()
)
// --- 3. ログアウト設定 ---
.logout(logout -> logout
.logoutUrl("/logout")
.invalidateHttpSession(true) // セッション無効化
.deleteCookies("JSESSIONID") // クッキー削除
.logoutSuccessUrl("/login") // ログアウト後の遷移先
);
// ※CSRF対策やヘッダー設定はデフォルトで有効なため、記述しなくてOKです
return http.build();
}
}
設定のポイント
Spring Security 6では、何も書かなければ「CSRF対策有効」「ヘッダーセキュリティ有効」 という最も安全な状態になります。
昔の記事で見かける .csrf(csrf -> csrf.disable()) は、セキュリティリスクが高まるため、API開発など明確な意図がある場合以外は記述すべきではありません。
Tips:ローカル開発でH2 Consoleを使いたい場合
実務のローカル開発などで、H2 Console(DB管理画面)を使用したい場合は、そのエンドポイントだけセキュリティの除外設定を行うのが推奨されます。
// H2 Consoleを使う場合の追記設定例
http.csrf(csrf -> csrf
// H2 ConsoleのパスだけCSRFチェックを除外
.ignoringRequestMatchers(PathRequest.toH2Console())
)
.headers(headers -> headers
// H2 ConsoleはiFrameを使うため、同一オリジンからの表示を許可
.frameOptions(frameOptions -> frameOptions.sameOrigin())
);
このように局所的に設定を緩めることで、アプリケーション全体の安全性を保つことができます。
補足:認証ロジックについて(UserDetailsService)
上記の設定クラスはあくまで「どのURLを誰に通すか(認可)」や「ログイン画面の挙動」を決める門番の設定です。
では、「DBからユーザー情報を取得してパスワードを照合する処理」はどこに書くのでしょうか?
それは、UserDetailsService というインターフェースを実装したクラスで行います。
- SecurityFilterChain: 「門番」。リクエストを通していいかチェックする。
- UserDetailsService: 「会員名簿」。送られてきたユーザーIDをDBから検索して返す役割。
実際のパスワード照合は、Spring Security内部のコンポーネント(AuthenticationManager / DaoAuthenticationProvider)が、この UserDetailsService から受け取った情報と入力値を突き合わせて行います。
// イメージ:UserDetailsServiceの実装
@Service
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// ここでDBからユーザーを検索し、UserDetails(Spring Security規定のユーザー型)に変換して返す
// パスワードの照合自体は、このメソッドの呼び出し元(Spring Security本体)が行う
}
}
まとめ
- Spring Securityは 「Filterの連なり」 で守られている。
- 現在は
SecurityFilterChainをBean定義して、ラムダ式で見通しよく設定を書くのが主流。 - 基本的にデフォルト設定が強力なので、「必要な設定だけを書く(余計な無効化をしない)」 ことが重要。
デフォルト機能が強力なので、まずは「どのFilterが何をしているか」を意識すると、エラー時の調査がスムーズになると思います。
参考
本記事の執筆にあたり、以下の公式ドキュメントおよび記事を参考にしました。
-
公式ドキュメント
-
Architecture :: Spring Security
- Spring Securityのアーキテクチャ全体像についての公式解説です。
-
Spring Security without the WebSecurityConfigurerAdapter
- Spring公式ブログによる、旧来の設定方法からの移行ガイドです。
-
Architecture :: Spring Security
-
技術記事
Discussion