SecurityFilterChainは、1つのリクエストに対してSpring Securityがどのフィルタをどの順で実行するかを決めるBeanです。Spring Security 6.0でWebSecurityConfigurerAdapterが削除されて以降、セキュリティ設定は原則このBeanを定義して書きます。ところが実際の設定でつまずくのは、書き方そのものよりも「既定でどのフィルタが走っているのか」「自作フィルタをどこに挟むのか」が見えない点です。この記事では、既定構成を起動ログとソースコードの登録順から確定させ、複数チェーンの切り分け、カスタムフィルタの挿入位置、Spring Security 7.0で置き換わった旧APIまでを整理します。バージョンの基準はSpring Security 7.1.1(Spring Boot 4.1.1が引き込む版・2026年9月時点の現行GAは7.1.1)です。
まとめ:SecurityFilterChainで押さえる6点
- SecurityFilterChainは「どのリクエストに、どのフィルタ列を当てるか」の1単位です。
FilterChainProxyが登録順に照合し、最初に一致した1本だけを実行します。2本目以降は評価されません。 - 既定構成のフィルタ名は起動時のDEBUGログに1行で出ます。推測でフィルタ順を語らず、この1行を読むのが最短です。
- 複数チェーンは
securityMatcherで範囲を絞り、@Orderで照合順を固定します。広いパターンのチェーンを先に置くと、後続が一切呼ばれません。 - カスタムフィルタに
@Componentを付けるとServletコンテナ側にも登録され、Spring Security側にも追加した場合は2回呼ばれることがあります。FilterRegistrationBeanのenabledをfalseにして止めます。 - 静的リソースは
WebSecurityCustomizerのignoringではなくpermitAllで通します。ignoringはセキュリティヘッダの付与ごと外れます。 - Spring Security 7.0で
AntPathRequestMatcherとauthorizeRequestsは削除、FilterSecurityInterceptorとAccessDecisionManagerはspring-security-accessへ移されて非推奨のまま残りました。5系や6系前半のサンプルはそのままではコンパイルが通りません。
SecurityFilterChainの役割とFilterChainProxyとの関係
SecurityFilterChainは単体で動くフィルタではなく、FilterChainProxyという1本のServlet Filterに束ねられて動きます。この二段構えを押さえていないと、フィルタが呼ばれない理由もログの出どころも追えません。
DelegatingFilterProxyによるServletコンテナとSpringの橋渡し
Servletコンテナに登録されるのはDelegatingFilterProxyです。公式リファレンスはこれを「ServletコンテナのライフサイクルとApplicationContextの間を橋渡しするFilter実装」と説明しています。コンテナ標準のFilter登録機構をそのまま使いながら、実処理はSpringのBeanとして定義されたフィルタへ委譲します。Bean名で遅延取得するため、Springのコンテキスト初期化が済む前にコンテナがフィルタを要求しても破綻しません。
このDelegatingFilterProxyが委譲する先がFilterChainProxyで、その内側に複数のSecurityFilterChainがぶら下がる、という三層になります。
FilterChainProxyを経由させる3つの実利
コンテナへ直接登録せずFilterChainProxyへ集約している理由を、公式リファレンスは3点挙げています。
- トラブルシュートの起点になる:Spring SecurityのServletサポートはすべてここから始まるため、挙動を追うときは
FilterChainProxyにブレークポイントを置けば足ります。 - メモリリークを防ぐ:リクエスト処理の終了時に
SecurityContextをクリアします。任意処理ではないため、フレームワーク側で必ず実行される場所が要ります。 - 照合の柔軟性が上がる:Servletコンテナのフィルタ登録はURLだけで呼び出しを決めますが、
FilterChainProxyはRequestMatcherを使い、HTTPメソッドやヘッダなどHttpServletRequestのあらゆる情報で判定できます。
あわせてHttpFirewallもここで適用され、パスの正規化を悪用した攻撃を入口で弾きます。Spring Security全体の位置づけはSpring Securityとは?認証・認可の仕組みと基本設定をわかりやすく解説で扱っています。
最初に一致した1本だけが実行される照合ルール
SecurityFilterChainを複数定義したときの挙動は、公式リファレンスが明言しています。/api/messages/へのリクエストが/api/**を持つチェーンに一致したなら、後続のチェーンも同じURLに一致するかどうかに関わらず、先に一致した1本だけが呼び出されます。
ここがSecurityFilterChainで最も事故が起きる箇所です。「APIだけステートレスにしたつもりが画面側の設定が効いていない」「管理画面のチェーンが無視される」という症状は、ほぼ照合順の問題です。なお各チェーンは独立して構成でき、フィルタを1本も持たないチェーンも定義できるため、フィルタ数の違うチェーンが並ぶこと自体は異常ではありません。
既定で組まれるフィルタの一覧と実行順序
起動時のDEBUGログで実構成を確認する手順
Spring Securityは各SecurityFilterChainの構成を、アプリケーション起動時にDEBUGレベルで1行にまとめて出力します。次の1行を設定ファイルに足すだけです。
logging.level.org.springframework.security=DEBUG
公式リファレンスが掲載している実際の出力は次の形です。Will secure any request withの後ろが、そのチェーンに実際に組まれたフィルタの実行順そのものになります。
2023-06-14T08:55:22.321-03:00 DEBUG 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [ DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter, BasicAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, ExceptionTranslationFilter, AuthorizationFilter]
この15本が、フォームログインとHTTP Basicを有効にした構成です。Spring Boot 4.1.1のServletWebSecurityAutoConfigurationが組む既定チェーンはauthorizeHttpRequests(anyRequest().authenticated())・formLogin・httpBasicの3つなので、設定を書かない状態はこの並びに一致します。自作フィルタを足したときの登録の成否も、この行に名前が出るかどうかで判断してください。チェーンを複数定義していれば、その数だけ同じ行が並びます。
ソースが定めるフィルタ登録順とその読み方
順序の正本はFilterOrderRegistrationクラスです。ここに登録された並びがそのまま実行順になります。Spring Security 7.0.xのソースでは41本のフィルタが登録されており、OAuth 2.0・SAML 2.0・CASといった別モジュール側のフィルタも、クラス名の文字列で同じ表に載っています。将来の追加に備えた予約スロットも2つ挟まっているため、通し番号ではなく前後関係で読むのが確実です。主要なフィルタを登録順のまま抜き出すと次のようになります。
| フィルタ(登録順の抜粋) | 担当 |
|---|---|
| WebAsyncManagerIntegrationFilter | 非同期処理へのコンテキスト伝播 |
| SecurityContextHolderFilter | SecurityContextの復元 |
| SecurityContextPersistenceFilter | 旧方式(非推奨) |
| HeaderWriterFilter | セキュリティヘッダ付与 |
| CorsFilter | CORS |
| CsrfFilter | CSRF検証 |
| LogoutFilter | ログアウト |
| UsernamePasswordAuthenticationFilter | フォーム認証 |
| BasicAuthenticationFilter | HTTP Basic認証 |
| RememberMeAuthenticationFilter | 自動ログイン |
| AnonymousAuthenticationFilter | 匿名ユーザーの補完 |
| ExceptionTranslationFilter | 例外のHTTP応答変換 |
| AuthorizationFilter | 認可判定 |
読み方の要点は2つあります。1つは認証フィルタ群(14から28)が認可フィルタ(31)より前にあること。認可判定の時点では認証結果がすでにSecurityContextHolderに入っている前提で書けます。もう1つはExceptionTranslationFilterがAuthorizationFilterの直前にあることです。認可の失敗はこの位置関係があるからこそ捕捉されます。
表の3行目、SecurityContextHolderFilterの直後に並ぶSecurityContextPersistenceFilterは登録表には残っています。ただしSpring Security 7.1.xのソースでも@Deprecatedが付いたままで、JavadocはSecurityContextHolderFilter(5.7で追加)を使うよう指示しています。既定構成のログにこの名前は出ません。古い解説記事がこのフィルタをセッション管理の主役として説明していることがありますが、現行の主役はその1つ前、SecurityContextHolderFilterのほうです。
自作フィルタを置く位置の判断基準
公式リファレンスは、自作フィルタの配置を「そのフィルタが動く時点で、どのイベントがすでに終わっている必要があるか」で決めるよう示しています。フィルタチェーンの主要イベントは、(1)セッションからのSecurityContext復元、(2)セキュアヘッダ・CORS・CSRFによる保護、(3)認証、(4)認可の順です。公式が挙げる目安は次のとおりです。
| 自作フィルタの性質 | この後ろに置く | 完了済みのイベント |
|---|---|---|
| 攻撃対策系 | SecurityContextHolderFilter | 1 |
| 認証系 | LogoutFilter | 1, 2 |
| 認可系 | AnonymousAuthenticationFilter | 1, 2, 3 |
独自の認証を足す場合、定位置はLogoutFilterの後ろです。JWTを検証するフィルタもここに入ります。SPAとトークン認証を組み合わせる構成はSpring BootとReactを連携する方法|REST API・CORS・JWT認証まで実装解説で扱っています。
SecurityFilterChain BeanのJava Config記述
設定は@ConfigurationクラスでSecurityFilterChainを返すBeanメソッドとして書きます。Spring Security 7.0でand()による連結が削除されたため、以下のラムダDSLが唯一の書き方です。
最小構成のBean定義
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.build();
}
}
HttpSecurityはビルダーで、最後のbuild()がSecurityFilterChainの実体を組み立てます。Customizer.withDefaults()は「その機能を既定設定のまま有効にする」という意味で、フォームログインのログイン画面もSpring Securityが自動生成します。
Spring BootでSpring Securityを依存に追加すると、この設定を1行も書かなくても既定のSecurityFilterChainが自動構成され、全リクエストが認証必須になります。自前のSecurityFilterChain Beanを1つでも定義した時点で、その自動構成は無効になります。
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/signup", "/css/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/api/orders").hasAuthority("SCOPE_orders:write")
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.logout(Customizer.withDefaults())
.build();
}
ルールは上から順に評価され、最初に一致したものが適用されます。そのためanyRequest()は必ず最後に置きます。順序を逆にすると個別ルールが一切効きません。hasRole("ADMIN")は内部でROLE_接頭辞を付けてROLE_ADMINを要求するのに対し、hasAuthorityは文字列をそのまま照合します。OAuth 2.0のスコープのように接頭辞を持たない権限を扱うときはhasAuthorityを使います。
マッチャの指定方法には版差があります。5系のantMatchersは6.0で削除され、requestMatchersに一本化されました。文字列で書くrequestMatchers("/admin/**")は7.0でもそのまま使えるので、通常の設定で版差に当たるのはAntPathRequestMatcherを明示的にnewしていた場合だけです(詳細は後段のAPI表)。
CSRFとセッションを用途に応じて切り替える
CSRF保護はSpring Securityの既定で有効です。ブラウザからフォーム送信する画面では有効のまま使い、トークン認証のREST APIのようにセッションCookieに依存しない経路でだけ無効化を検討します。無効化の是非とエラーの切り分けはInvalid CSRF tokenの意味と直し方|Laravel 419・Django・Spring Securityのエラーを切り分けるで詳しく扱っています。
@Bean
SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.build();
}
SessionCreationPolicy.STATELESSを指定すると、Spring Securityはセッションを作らず既存のセッションも参照しません。リクエストごとに認証をやり直す構成になるため、csrf.disable()と組み合わせるのはこの前提が成立するときだけにします。フォーム認証の画面をこの設定にすると、ログイン直後に認証状態が消えます。
URL単位でポリシーを分ける複数チェーンの構成
REST APIと画面を1つのアプリケーションで提供する場合、認可ルールだけでなくセッション方針やCSRFの扱いまで変わります。これはauthorizeHttpRequestsの中では表現できないため、SecurityFilterChainごと分けます。
securityMatcherと@Orderによる切り分け
@Bean
@Order(1)
SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.build();
}
@Bean
@Order(2)
SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/css/**").permitAll()
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.build();
}
securityMatcherが決めるのはそのチェーン自体を適用するかどうかです。「チェーン内でどの権限を要求するか」を決めるauthorizeHttpRequestsの中のrequestMatchersとは役割が違います。ここを混同すると、APIのチェーンが画面のリクエストまで飲み込みます。
@Orderが決めるのは照合順です。securityMatcherを持たないチェーンは全リクエストに一致するので、置き場所は必ず最後(この例では2番)になります。順番を逆にすると/api/**のリクエストも画面用チェーンに吸われ、APIがフォームログイン画面へリダイレクトされます。@Orderの付け忘れは各Beanの優先度を最低値で横並びにするため、定義順に依存する不安定な構成を招く点にも注意してください。複数チェーンを定義するときは全てに@Orderを付けるのが安全です。
カスタムフィルタの挿入位置と二重実行の回避
独自の認証やテナント判定を差し込むときは、フィルタを自作してチェーンに登録します。論点は順序の指定と、Spring Boot固有の登録の罠の2つです。
addFilterBefore・addFilterAfter・addFilterAtのAPI差
HttpSecurityには登録用のメソッドが4種類あります。7.0.xのソースで確認できるシグネチャは次のとおりです。
| メソッド | 引数 | 挿入位置 |
|---|---|---|
| addFilterBefore | Filter, Class | 指定フィルタの直前 |
| addFilterAfter | Filter, Class | 指定フィルタの直後 |
| addFilterAt | Filter, Class | 指定フィルタと同じ位置 |
| addFilter | Filter | 既知フィルタの登録順に従う |
第2引数の基準クラスには、FilterOrderRegistrationに登録済みのフィルタを指定します。addFilterAtが行うのは同じ位置への追加だけで、既存フィルタの置き換えではありません。標準フィルタを差し替えたい場合は、対象の機能をDSLで無効化してから使ってください。引数なしのaddFilterはSpring Securityが順序を知っているフィルタにしか使えず、完全な自作クラスを渡すと例外になります。位置を自分で決める場面ではaddFilterBeforeかaddFilterAfterを選びます。
テナントIDヘッダを検証する認可系フィルタなら、公式の判断基準に従ってAnonymousAuthenticationFilterの後ろに置きます。
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.access.AccessDeniedException;
public class TenantFilter implements Filter {
private final TenantAccessChecker checker;
public TenantFilter(TenantAccessChecker checker) {
this.checker = checker;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String tenantId = request.getHeader("X-Tenant-Id");
if (this.checker.isUserAllowed(tenantId)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
throw new AccessDeniedException("Access denied");
}
}
判定そのものはTenantAccessCheckerのような別のクラスへ切り出します(boolean isUserAllowed(String tenantId)を1つ持つインターフェースで足ります)。フィルタ側に判定ロジックを直接書くと、後述する二重登録の回避でこのクラスをBeanにしたときに責務が膨らみます。
@Bean
SecurityFilterChain filterChain(HttpSecurity http, TenantFilter tenantFilter) throws Exception {
return http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.addFilterAfter(tenantFilter, AnonymousAuthenticationFilter.class)
.build();
}
ここでAccessDeniedExceptionを投げているのは意図的です。この例外は後段ではなく前段のExceptionTranslationFilterが捕まえて、設定済みのハンドラで応答へ変換します。フィルタの中で直接response.sendErrorを書くと、この一元化された経路を外れます。
@Componentが招く二重実行とFilterRegistrationBeanでの停止
自作フィルタに@Componentを付けたくなりますが、Spring Bootではこれが事故になります。公式リファレンスは次のように明記しています。Filterを@Componentで、あるいは設定クラスでBeanとして宣言すると、Spring Bootがそれを組込コンテナへ自動登録するため、コンテナ側とSpring Security側で計2回、しかも異なる順序で呼び出されることがある、というものです。Spring Boot側のリファレンスも「Spring Beanである任意のServlet、Filter、Listenerは組込コンテナに登録される」と述べており、両者は同じ挙動を指しています。
症状は分かりにくい形で出ます。認証フィルタなら認証処理が2回走り、カウンタやレート制限のフィルタなら計上が倍になり、コンテナ側の呼び出しはSpring Securityのチェーンの外側なのでSecurityContextがまだ空です。「フィルタの中でユーザー情報が取れない実行と取れる実行が混ざる」という現象があれば、フィルタの二重登録の有無を確認します。
公式が示す回避策は、フィルタをBeanのままにしつつコンテナ側の登録だけを止めることです。
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
これでフィルタを追加するのはHttpSecurityだけになり、依存性注入は使えたまま二重実行が消えます。判断としては、依存注入が要らないフィルタはBeanにせずnewで渡すのが最も単純です。公式リファレンスも「そのためフィルタはSpring Beanでないことが多い」と書いています。DIが必要な場合にだけ、上のFilterRegistrationBeanを1つ足してください。
Spring Bootの自動設定と静的リソースの扱い
Spring BootはFilterChainProxy自体をコンテナへ登録する部分も受け持ちます。この層を押さえると、CSSや画像が401になる問題や、他のフィルタとの前後関係を調整できます。
ignoringではなくpermitAllを使う理由
静的リソースをSpring Securityの対象外にする方法として、WebSecurityCustomizerのignoringがよく使われます。しかし公式リファレンスは「Favor permitAll over ignoring」という見出しを立てて、これを避けるよう明確に推奨しています。根拠は逐語で次のとおりです。
「静的リソースがある場合、フィルタチェーンにそれらを無視させたくなることがあります。より安全なのはpermitAllで許可する方法です」。理由として「静的リソースであってもセキュアなヘッダを書き出すことは重要であり、リクエストが無視されるとSpring Securityはそれを行えないため、より安全である」と述べています。ignoringで外れるのは認証だけではなく、HeaderWriterFilterによるセキュリティヘッダの付与やHttpFirewallの保護までまとめて外れる、ということです。
かつてはpermitAllにすると全リクエストでセッションが参照される性能上のトレードオフがありました。公式リファレンスはこの点についても「Spring Security 6以降、認可ルールが必要としない限りセッションは参照されなくなった。性能上の影響が解消されたため、Spring Securityはすべてのリクエストに対して少なくともpermitAllを使うことを推奨する」と明記しています。6以降では、静的リソースにもセキュリティヘッダを付与できるpermitAllを通常は優先します。
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.build();
}
spring.security.filter.orderの既定値と適用範囲
Spring Securityのフィルタチェーンが、アプリケーションの他のServlet Filterに対してどの位置に入るかはspring.security.filter.orderで決まります。Spring Boot 4.1.1のソースではSecurityFilterPropertiesにこの既定値が定義されており、OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER - 100、つまり-100です。値が小さいほど先に実行されるため、既定では多くのアプリケーション独自フィルタより前に走ります。
もう1つのspring.security.filter.dispatcher-typesは、どのディスパッチャタイプでフィルタを適用するかを指定します。既定はEnumSet.allOf(DispatcherType.class)で、REQUESTだけでなくFORWARD・INCLUDE・ERROR・ASYNCのすべてが対象です。エラーページへのフォワードでも認可が効くのはこのためで、/errorを明示的に許可していないと例外時に想定外の応答になることがあります。
なお設定キーは3系と4系で同じですが、実装クラスの位置が変わっています。Spring Boot 3系ではこれらはSecurityPropertiesの入れ子クラスFilterでしたが、4系ではオートコンフィグレーションのモジュール分割にともないSecurityFilterPropertiesという独立クラスへ移りました。SecurityPropertiesを直接参照していたコードは4系で調整が必要です。
ExceptionTranslationFilterによる認証・認可エラーの振り分け
ログイン画面へのリダイレクト、401、403の出し分けは、コントローラではなくフィルタ層のExceptionTranslationFilterが決めます。
AuthenticationEntryPointとAccessDeniedHandlerの役割分担
公式リファレンスが示す疑似コードでは、このフィルタはfilterChain.doFilterをtryで囲み、後続から飛んできたAuthenticationExceptionとAccessDeniedExceptionを捕まえます。分岐は次の2通りです。
- 未認証、または
AuthenticationExceptionの場合:SecurityContextHolderをクリアし、元のリクエストをRequestCacheへ保存したうえで、AuthenticationEntryPointで認証情報を要求します。ログイン画面へのリダイレクトやWWW-Authenticateヘッダの送出がここに当たります。保存したリクエストは認証成功後に再現されます。 - 認証済みで
AccessDeniedExceptionの場合:AccessDeniedHandlerが呼ばれ、403を返すか専用画面へ遷移します。
つまりAuthenticationExceptionなら認証要求へ進み、AccessDeniedExceptionなら匿名か認証済みかによって認証要求と403に分岐します。ログインしていないユーザーが管理画面を叩けばログイン画面へ、ログイン済みで権限が足りないユーザーが叩けば403へ、という挙動はこの分岐が生んでいます。どちらの例外も投げられなければ、このフィルタは何もしません。
@Bean
SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.accessDeniedHandler((request, response, denied) ->
response.sendError(HttpServletResponse.SC_FORBIDDEN)))
.build();
}
REST APIではログイン画面へのリダイレクトが不都合なので、HttpStatusEntryPointを使えば、ログイン画面へリダイレクトせず401を返せます。
認可の失敗が例外として戻ってくる仕組み
登録順の表で確認したとおり、ExceptionTranslationFilterはAuthorizationFilterの直前にあり、例外変換の方が先です。フィルタチェーンでは前段のフィルタが後段をdoFilterで呼び出す入れ子構造になるため、後段のAuthorizationFilterが投げた例外は呼び出し元である前段へ戻ります。
この構造のおかげで、認可の失敗もメソッドセキュリティ(@PreAuthorizeなど)の失敗も、コントローラ内で発生した認証例外も、すべて同じ1箇所で応答へ変換されます。例外ハンドリングを分散させずに済むのはこの位置関係の効果です。逆に言えば、ExceptionTranslationFilterより前に置いたフィルタが投げた例外は捕捉されません。自作フィルタで認可エラーを表現したいなら、公式が示すとおりAnonymousAuthenticationFilterの後ろ、つまりExceptionTranslationFilterより後段に置く必要があります。
Spring Security 7とSpring Boot 4で置き換わったAPI
SecurityFilterChainの解説記事は5系から6系への移行期に書かれたものが多く、7.0の変更に追随していないものが少なくありません。参照しているサンプルコードがそのままコンパイルできるかどうかは、まず使っているバージョンの確認からです。
Spring BootとSpring Securityのバージョン対応
Spring Bootが実際に引き込むSpring Securityのバージョンは、spring-boot-dependenciesのPOMにあるspring-security.versionプロパティが正本です。2026年9月9日時点で実測した値は次のとおりです。
| Spring Boot | Spring Security | 備考 |
|---|---|---|
| 3.5.9 | 6.5.7 | 3.5系はOSSサポート終了済み |
| 4.0.2 | 7.0.2 | 7.0で旧API削除 |
| 4.1.0 | 7.1.0 | Spring Security現行GA系 |
| 4.1.1 | 7.1.1 | Spring Boot現行GA |
Spring Boot側のサポート期限と系列の選び方はSpring Bootバージョン一覧とサポート期限に、4系の変更点全体はSpring Boot 4とは?最新バージョン4.1の変更点・新機能とSpring Boot 3との違いを解説にまとめています。
削除・非推奨になったAPIと代替
版帰属は、Spring Securityの各ブランチのソースツリーを直接確認して確定しました(spring-projects/spring-securityの6.0.x・6.5.x・7.0.x・7.1.x)。「削除」と「非推奨のまま別モジュールへ移動」は意味が違うので、分けて示します。
| 旧API | 状態 | 代替 |
|---|---|---|
| WebSecurityConfigurerAdapter | 6.0で削除 | SecurityFilterChain Bean |
| antMatchers / mvcMatchers | 6.0で削除 | requestMatchers |
| authorizeRequests | 6.1で非推奨・7.0で削除 | authorizeHttpRequests |
| HttpSecurity.and() | 7.0で削除 | ラムダDSL |
| AntPathRequestMatcher | 6.5で非推奨・7.0で削除 | PathPatternRequestMatcher |
| FilterSecurityInterceptor | 7.0でspring-security-accessへ移動・非推奨 | AuthorizationFilter |
| AccessDecisionManager | 7.0でspring-security-accessへ移動・非推奨 | AuthorizationManager |
| SecurityContextPersistenceFilter | 非推奨(7.1.xにも存在) | SecurityContextHolderFilter |
下3行の扱いには注意が要ります。FilterSecurityInterceptorとAccessDecisionManagerはクラス自体が消えたわけではなく、7.0で新設されたspring-security-accessという別アーティファクトへ移されました(Maven Centralでの初出は7.0.0系)。移った先でも@Deprecatedが付いており、Javadocの指示先はそれぞれAuthorizationFilterとAuthorizationManagerです。spring-boot-starter-security 4.1.1が引き込むのはspring-security-configとspring-security-webだけなので、これらの旧APIを使い続けるにはspring-security-accessを自分で依存に足す必要があります。つまり「7.0に上げたらコンパイルが通らない」場合、依存追加で延命するか、AuthorizationManagerへ書き換えるかの選択になります。延命は移行猶予のための手段で、新規実装で選ぶものではありません。
押さえておきたいのは、変更が6.0と7.0の2回に分かれている点です。6.0で変わったのは設定の書き方、7.0で動いたのは認可の実装そのものとDSLの旧形式でした。5系から7系へ一気に上げると両方が同時に降ってきます。公式が6.5を経由する段階的な手順を推奨しているのはこのためです。逆に、SecurityFilterChain Beanを定義してラムダDSLで書いているコードは7.0でもほぼそのまま動きます。6.0の時点で現行の書き方に移行済みなら、7.0で追加の書き換えが要るのは、AccessDecisionManagerやAntPathRequestMatcherを明示的に使っていた箇所などに限られるはずです。実装レベルの移行手順はSpring Bootの認証をSpring Securityで実装する方法【Spring Security 6/7対応】で扱っています。
よくある質問
Spring Bootの既定ではどのフィルタが有効になっていますか?
構成はバージョンと有効化した機能で変わるため、固定の一覧を覚える意味はありません。logging.level.org.springframework.security=DEBUGを入れて起動し、Will secure any request withに続く配列を読んでください。
addFilterBeforeとaddFilterAfterはどう使い分けますか?
基準フィルタの前に入れるか後に入れるかの違いだけで、選び方は「そのフィルタが動く時点で完了していてほしいイベントは何か」に集約できます。同じ位置をLogoutFilterの後ろともUsernamePasswordAuthenticationFilterの前とも表現できるので、どちらのメソッドを使うかは本質ではありません。
SecurityFilterChainでCSRFを無効化しても問題ありませんか?
セッションCookieで認証している経路では無効化しないでください。CSRF攻撃はブラウザが自動送信するCookieを悪用するため、Cookie認証とCSRF保護は対になっています。無効化を検討してよいのは、認証情報を毎回Authorizationヘッダで送るトークン認証のAPIのように、Cookieに依存しない経路だけです。
Servlet標準のFilterChainとSecurityFilterChainは何が違いますか?
Servlet仕様のFilterChainはコンテナが管理する汎用の連鎖で、呼び出しがURLだけで決まります。SecurityFilterChainはSpringのBeanで、FilterChainProxyの内側に構成され、RequestMatcherによってHTTPメソッドやヘッダを含む任意の条件で適用可否を判定できる点が違います。
自作フィルタが2回実行されるのはなぜですか?
フィルタを@ComponentやBean定義でSpring Beanにしたうえで、HttpSecurityにも追加しているためです。コンテナ側の呼び出しはSpring Securityのチェーン外なので、そちらではSecurityContextが空になります。DIが不要ならBeanにせずnewで渡し、DIが要るならFilterRegistrationBeanのsetEnabled(false)でコンテナ登録だけを止めてください。
関連記事
- Spring Securityとは?認証・認可の仕組みと基本設定をわかりやすく解説【2026年版】
- Spring Bootの認証をSpring Securityで実装する方法【Spring Security 6/7対応・2026年版】
- Spring Boot 4とは?最新バージョン4.1の変更点・新機能とSpring Boot 3との違いを解説【2026年最新】
- Invalid CSRF tokenの意味と直し方|Laravel 419・Django・Spring Securityのエラーを切り分ける
- 【2026年版】Spring BootとReactを連携する方法|REST API・CORS・JWT認証まで実装解説