SecurityFilterChainは、1つのリクエストに対してSpring Securityがどのフィルタをどの順で実行するかを決めるBeanです。Spring Security 6.0でWebSecurityConfigurerAdapterが削除されてからは、セキュリティ設定はこのBeanを定義して書きます。つまずくのは書き方よりも、「既定でどのフィルタが走っているのか」「自作フィルタをどこに挟めば期待どおり動くのか」が見えない点です。この記事では、Spring Boot 4.1.1(Spring Security 7.1.1)・Java 25の最小アプリを起動して確かめたログと応答コードをもとに、既定構成、複数チェーン、APIキー認証フィルタの置き場所、フィルタのBean登録で起きる事故、7.0で置き換わった旧APIまでを整理します。
まとめ:SecurityFilterChainで押さえる7点
- SecurityFilterChainは「どのリクエストに、どのフィルタ列を当てるか」の1単位です。
FilterChainProxyが順に照合し、最初に一致した1本だけを実行します。 - Spring Boot 4.1.1の既定チェーンは16本です。公式リファレンスに載っている2023年のログ例は15本で、6.4で加わった
DefaultResourcesFilterがありません。実構成は起動時のDEBUGログ1行で確認します。 - 複数チェーンは
securityMatcherで範囲を絞り、@Orderで照合順を固定します。範囲を絞らないチェーンは必ず最後に置きます。 - APIキーやトークンを検証する認証フィルタは
LogoutFilterの後ろ、認証結果を読むフィルタはAnonymousAuthenticationFilterの後ろに置きます。 AccessDeniedExceptionを投げるフィルタはExceptionTranslationFilterの後ろに置きます。AnonymousAuthenticationFilterの直後では例外が捕まらず、500になりました。- 自作フィルタをBeanにするとSpring Bootがコンテナにも登録します。コンテナ側の呼び出しはSpring Securityの後に全URLで走り、ログイン中のユーザーが別の主体にすり替わることがあります。
newで渡すか、FilterRegistrationBeanで止めます。 - Spring Security 7.0で
AntPathRequestMatcherとauthorizeRequestsは削除され、FilterSecurityInterceptorとAccessDecisionManagerはspring-security-accessへ移されて非推奨のまま残りました。
SecurityFilterChainの役割とFilterChainProxyとの関係
SecurityFilterChainは単体で動くフィルタではなく、FilterChainProxyという1本のServlet Filterに束ねられて動きます。この二段構えを押さえていないと、フィルタが呼ばれない理由もログの出どころも追えません。
DelegatingFilterProxyによるServletコンテナとSpringの橋渡し
Servletコンテナに登録されるのはDelegatingFilterProxyです。Spring Security公式リファレンスのアーキテクチャ解説は、これを「ServletコンテナのライフサイクルとApplicationContextの間を橋渡しするFilter実装」と説明しています。コンテナ標準の登録機構を使いながら、実処理はBeanとして定義されたフィルタへ委譲します。Bean名で遅延取得するため、コンテキストの初期化前にコンテナがフィルタを要求しても破綻しません。委譲先がFilterChainProxyで、その内側に複数のSecurityFilterChainがぶら下がる三層構造です。
FilterChainProxyを経由させる3つの実利
- トラブルシュートの起点になる:Servlet向けの処理はすべてここから始まるため、挙動を追うときは
FilterChainProxyにブレークポイントを置けば足ります。 - メモリリークを防ぐ:リクエスト処理の終了時に
SecurityContextをクリアします。 - 照合の条件を広げられる:コンテナのフィルタ登録はURLパターン・Servlet名・ディスパッチャタイプで呼び出しを決めますが、
FilterChainProxyはRequestMatcherでHTTPメソッドやヘッダまで判定に使えます。
パスの正規化を悪用した攻撃を弾くHttpFirewallもここで適用されます。Spring Security全体の位置づけはSpring Securityとは?認証・認可の仕組みと基本設定をわかりやすく解説で扱っています。
最初に一致した1本だけが実行される照合ルール
公式リファレンスの例では、/api/messages/へのリクエストが/api/**のチェーンに一致すると、後続のチェーンが同じURLに一致するかどうかに関わらず、先に一致した1本だけが呼び出されます。「APIだけステートレスにしたつもりが画面側の設定が効いていない」「管理画面のチェーンが無視される」という症状が出たら、まず照合順を疑い、起動ログでチェーンの並びを確認します。チェーンごとにフィルタの本数が違うのは正常で、後述の実測でもAPI用は13本、画面用は16本でした。
Spring Boot 4.1の既定チェーンに並ぶ16本のフィルタ
Spring Securityを依存に足しただけの状態では、Spring BootのServletWebSecurityAutoConfigurationがauthorizeHttpRequests(anyRequest().authenticated())・formLogin・httpBasicの3つで既定チェーンを組みます。この自動構成は@ConditionalOnMissingBean(SecurityFilterChain.class)付きなので、自前のSecurityFilterChain Beanを1つでも定義した時点で無効になります。
起動時のDEBUGログで実構成を確認する手順
Spring Securityは各チェーンの構成を起動時にDEBUGレベルで1行にまとめて出力します。設定ファイルに次の1行を足します。
logging.level.org.springframework.security=DEBUG
Spring Boot 4.1.1でspring-boot-starter-webmvcとspring-boot-starter-securityだけを入れ、設定クラスを書かずに起動したときの出力です(日時とプロセスIDは省略)。
o.s.s.web.DefaultSecurityFilterChain : Will secure any request with filters: DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter, DefaultResourcesFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter, BasicAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, ExceptionTranslationFilter, AuthorizationFilter
並んでいる16本が、そのまま実行順です。公式リファレンスに載っているログ例は2023年6月時点のもので、15本しかなく、書式もwith [ ... ]の角括弧形式でした。差はDefaultResourcesFilterの1本で、自動生成されるログイン画面のCSSを配信するフィルタです。Spring Securityのソースでは6.4系で初めて登場します。解説記事の一覧と手元のログが合わないときは、記事の版を疑ってください。自作フィルタが登録されたかどうかも、この行に名前が出るかで確認できます(実際に呼ばれたかどうかは、リクエスト単位のTRACEログで確かめます)。
この状態での応答は、ブラウザ相当のAccept: text/htmlなら/loginへの302、それ以外は401とWWW-Authenticate: Basic realm="Realm"でした。同じ認証必須でもリダイレクトと401が分かれるのは、formLoginとhttpBasicのエントリポイントがAcceptヘッダで使い分けられているためです。
ソースが定めるフィルタ登録順とその読み方
順序の正本はFilterOrderRegistrationのソース(7.1.x)です。7.1.xでは41本が登録され、OAuth 2.0・SAML 2.0・CASなど別モジュールのフィルタもクラス名の文字列で同じ表に載っています。予約スロットも2つ挟まるため、通し番号ではなく前後関係で読むのが確実です。主要なものを登録順に抜き出します。
| フィルタ(登録順の抜粋) | 担当 |
|---|---|
| SecurityContextHolderFilter | SecurityContextの復元 |
| HeaderWriterFilter | セキュリティヘッダ付与 |
| CorsFilter | CORS |
| CsrfFilter | CSRF検証 |
| LogoutFilter | ログアウト |
| UsernamePasswordAuthenticationFilter | フォーム認証 |
| BasicAuthenticationFilter | HTTP Basic認証 |
| RememberMeAuthenticationFilter | 自動ログイン |
| AnonymousAuthenticationFilter | 匿名ユーザーの補完 |
| SessionManagementFilter | セッション管理 |
| ExceptionTranslationFilter | 例外のHTTP応答変換 |
| AuthorizationFilter | 認可判定 |
読み方の要点は2つです。1つは、既定構成では認証系のフィルタがすべてAuthorizationFilterより前にあること。認可判定の時点で認証結果はSecurityContextHolderに入っています(主体を切り替えるSwitchUserFilterだけはAuthorizationFilterの後ろに登録されています)。もう1つは、AnonymousAuthenticationFilterとExceptionTranslationFilterの間にSessionManagementFilterなどが挟まっていることです。後述するとおり、この隙間に置いたフィルタが投げた例外は応答に変換されません。旧方式のSecurityContextPersistenceFilterも表には残っていますが、7.1.xでも@Deprecatedのままで、既定構成のログには出ません。
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();
}
}
最後のbuild()がSecurityFilterChainの実体を組み立てます。Customizer.withDefaults()は「その機能を既定設定のまま有効にする」という意味で、ログイン画面もSpring Securityが自動生成します。
@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は文字列をそのまま照合します。SCOPE_で始まるOAuth 2.0のスコープのように、ROLE_を補われては困る権限はhasAuthorityで扱います。5系のantMatchersは6.0で削除され、文字列で書くrequestMatchers("/admin/**")は7.0でもそのまま使えます。
CSRF保護とセッション方針の用途別設定
CSRF保護は既定で有効です。ブラウザからフォーム送信する画面では有効のまま使い、ブラウザが認証情報を自動で送らない経路(Authorizationヘッダでトークンを送るAPIなど)でだけ無効化を検討します。エラーの切り分けは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())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.build();
}
SessionCreationPolicy.STATELESSを指定すると、Spring Securityはセッションを作らず、既存のセッションがあってもSecurityContextの取得には使いません。ただしCSRF保護を外してよいかはセッションの有無ではなく、ブラウザが認証情報を自動で送るかどうかで決まります。上の例はAuthorizationヘッダでJWTを受け取るリソースサーバー(spring-boot-starter-oauth2-resource-serverが必要)なので無効化できますが、セッションCookieやHTTP Basicのようにブラウザが自動送信する認証情報を受け付ける経路では、STATELESSでも有効のままにします。フォーム認証の画面をSTATELESSにすると、ログイン直後に認証状態が消えます。
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.anyRequest().hasRole("API"))
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)))
.addFilterAfter(new ApiKeyFilter(), LogoutFilter.class)
.addFilterAfter(new AuditFilter(), AnonymousAuthenticationFilter.class)
.build();
}
@Bean
@Order(2)
SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.build();
}
この2本で起動すると、DEBUGログは2行になります。API用の1行目はWill secure Or [PathPattern [/api/**]] with filters:で始まり、CSRF・フォームログイン・ログイン画面生成のフィルタが消え、代わりに自作のApiKeyFilterとAuditFilter(中身は後述)、STATELESS指定で入るSessionManagementFilterを含む13本でした。2行目の画面用は既定と同じ16本です。
securityMatcherが決めるのはそのチェーン自体を適用するかどうかで、チェーン内でどの権限を要求するかを決めるrequestMatchersとは役割が違います。@Orderは照合順です。securityMatcherを持たないチェーンは全リクエストに一致するので、必ず最後に置きます。順番を逆にすると、7.1.1では起動時にUnreachableFilterChainExceptionが出てアプリケーションが立ち上がりませんでした。全リクエストに一致するチェーンの後ろに置いたチェーンは決して呼ばれない、という構成検証です。複数チェーンを定義するときは全てに@Orderを付けてください。Actuatorのエンドポイントを別チェーンで守る構成はSpring Boot Actuatorの設定|exposure.includeとaccessの使い分け【4.1対応】で扱っています。
カスタムフィルタを追加する位置と手順
独自の認証や監査ログを差し込むときは、フィルタを自作してチェーンに登録します。置く位置を間違えても起動は成功するので、ログと応答コードで確かめるところまでが手順です。
位置を決める基準とaddFilterBefore・addFilterAfter・addFilterAtの違い
公式リファレンスは、自作フィルタの位置を「そのフィルタが動く時点で、どのイベントが終わっている必要があるか」で決めるよう示しています。イベントは(1)SecurityContextの復元、(2)ヘッダ・CORS・CSRFによる保護、(3)認証、(4)認可の順です。
| 自作フィルタの性質 | この後ろに置く | 完了済みのイベント |
|---|---|---|
| 攻撃対策系 | SecurityContextHolderFilter | 1 |
| 認証系 | LogoutFilter | 1, 2 |
| 認証結果を読む系 | AnonymousAuthenticationFilter | 1, 2, 3 |
| 例外で拒否する系 | ExceptionTranslationFilter | 1, 2, 3 |
公式の表は3行目を「認可系」としていますが、例外を投げて拒否するフィルタはその位置では動きません(後述の実測)。そのため4行目を分けています。登録メソッドはaddFilterBefore(基準の直前)、addFilterAfter(基準の直後)、addFilterAt(基準と同じ位置)、引数1つのaddFilter(Spring Securityが順序を知っているフィルタ専用で、未知のクラスを渡すとIllegalArgumentException)の4つです。既定構成ではLogoutFilterの直後とUsernamePasswordAuthenticationFilterの直前は隣り合っていますが、登録表ではその間にX.509・CAS・OAuth 2.0ログイン・SAML 2.0の認証フィルタの枠があり、これらを有効にすると両者は別の位置になります。BeforeかAfterかより、どのフィルタを基準にするかが本質です。
Web API向け認証フィルタのLogoutFilter直後への配置
X-API-KEYヘッダを検証して認証済みにするフィルタの例です。1リクエストで1回だけ動くようOncePerRequestFilterを継承します。
public class ApiKeyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
if ("secret-key".equals(req.getHeader("X-API-KEY"))) {
var auth = UsernamePasswordAuthenticationToken.authenticated(
"api-client", null, AuthorityUtils.createAuthorityList("ROLE_API"));
SecurityContext ctx = SecurityContextHolder.createEmptyContext();
ctx.setAuthentication(auth);
SecurityContextHolder.setContext(ctx);
}
chain.doFilter(req, res);
}
}
キーが無くてもここでは拒否せず、そのまま次へ渡すのが要点です。拒否は後段のAuthorizationFilterに任せます。前節のapiFilterChainに登録して呼び出した結果は次のとおりでした。200の本文は、検証用のコントローラがPrincipalの名前にapi:を付けて返したものです。
| リクエスト | 応答 |
|---|---|
| GET /api/hello(ヘッダなし) | 401 |
| GET /api/hello(X-API-KEY: secret-key) | 200 api:api-client |
| GET /web/hello(X-API-KEY: secret-key) | 401(画面用チェーンには無い) |
キーを固定文字列で比べているのは説明のためで、実運用ではハッシュ化した値を保存し、比較にも時間差の出ない方法を使います。トークンの発行や失効まで含めた認証基盤を設計するなら、認証基盤・ID管理システム開発の支援内容も判断材料になります。JWTを検証するフィルタも同じ位置に入ります。SPAとJWTの組み合わせはSpring BootとReactの連携方法|REST API・CORS・JWT認証から1つのjarへの同梱まで【Spring Boot 4.1】で扱っています。
認証後フィルタによるSecurityContextの参照
監査ログやテナント判定のように「誰のリクエストか」を知りたいフィルタは、最後の認証フィルタであるAnonymousAuthenticationFilterの後ろに置きます。
public class AuditFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(AuditFilter.class);
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
Authentication a = SecurityContextHolder.getContext().getAuthentication();
log.info("uri={} auth={}", ((HttpServletRequest) req).getRequestURI(), a.getName());
chain.doFilter(req, res);
}
}
.addFilterAfter(new AuditFilter(), AnonymousAuthenticationFilter.class)で登録すると、キーなしのリクエストではAnonymousAuthenticationToken(名前はanonymousUser)、キーありではApiKeyFilterが入れたapi-clientが読めました。匿名認証が有効な既定のままなら、この位置でgetAuthentication()がnullになることはなく、未認証は匿名トークンとして届きます(anonymous(a -> a.disable())にした構成ではnullになり得ます)。「未ログインならnull」という前提で書いた判定は、ここでは常に偽になります。
AccessDeniedExceptionを投げるフィルタの配置条件
公式リファレンスのテナント判定の例は、AnonymousAuthenticationFilterの後ろに置いたフィルタからAccessDeniedExceptionを投げています。同じ構成で、ヘッダが不正なら例外を投げるフィルタを2つの位置に置き、Basic認証でuserとしてログインした状態で比べました。
| 登録の基準 | チェーン内の位置 | 拒否時の応答 |
|---|---|---|
| addFilterAfter(…, AnonymousAuthenticationFilter.class) | ExceptionTranslationFilterの前 | 500 |
| addFilterAfter(…, ExceptionTranslationFilter.class) | ExceptionTranslationFilterの後 | 403 |
前者では例外がコンテナまで抜け、Tomcatの500ページとスタックトレースが出ました。ExceptionTranslationFilterは自分より後段のdoFilterをtryで囲んでいるだけなので、手前で投げられた例外は捕まえられません。なお後者の位置でも、未ログインのリクエストなら403ではなく認証の要求(この構成では401)に分岐します。例外で拒否するならExceptionTranslationFilterの後ろ、例外を使わずに応答を書くならAnonymousAuthenticationFilterの後ろと使い分けてください。
addFilterAtで同種のフィルタを重ねたときの挙動
公式リファレンスはaddFilterAtを「別のフィルタを置き換える」と説明し、httpBasic()とaddFilterAt(…, BasicAuthenticationFilter.class)を併用すると例外になると書いています。しかし7.1.1で両方を書いて起動すると、例外は出ず、ログにBasicAuthenticationFilter, BasicAuthenticationFilterと2本並びました。公式ドキュメントのテストコードCustomizingFilterTestsも、この誤った構成で「両方のフィルタが存在する」ことを確かめる内容です。addFilterAtは置き換えではなく同じ位置への追加なので、標準フィルタを差し替えるときは、対応するDSL(httpBasicなど)を書かないかdisable()してから使います。
フィルタのBean登録が招く二重実行と主体のすり替わり
自作フィルタに@Componentを付けたり@Beanで定義したりすると、Spring Bootはそれを組込コンテナにも登録します。Spring Bootリファレンスは「Spring BeanであるServlet、Filter、Listenerは組込コンテナに登録される」と書いており、Spring Securityの公式リファレンスも「コンテナとSpring Securityで2回、異なる順序で呼ばれることがある」と注意しています。
既定のコンテナ登録における実行順とURL範囲
前述のApiKeyFilterとAuditFilterをBeanとして定義し、API用チェーンにも渡して動かしました。
- 呼ばれる順:Spring Securityのフィルタ(
spring.security.filter.orderの既定は-100)が先に走り、Bean由来のコンテナ登録フィルタはその後でした。コンテナ側のAuditFilterは認証済みの主体を読めており、「コンテナ側ではSecurityContextが空」になるわけではありません。 - /api/hello:
AuditFilterは2回呼ばれました。ApiKeyFilterはOncePerRequestFilterの重複防止で2回目が飛ばされ、1回でした。 - /web/hello:API用チェーンにしか入れていないはずの2本が、画面のリクエストでも動きました。
最後の点が事故になります。画面にBasic認証でuserとしてログインし、同時にX-API-KEYヘッダを付けて/web/helloを呼ぶと、コントローラが受け取ったPrincipalはapi-clientでした。認可判定はuserとして済んだ後に、コンテナ側のApiKeyFilterがSecurityContextを上書きしたためです。権限の判定とコントローラで使う主体が食い違う状態は、監査ログの記録者や操作者の取り違えにつながります。
FilterRegistrationBeanによるコンテナ登録の無効化
依存性注入が要らないフィルタは、BeanにせずnewでHttpSecurityへ渡すのが最も単純です。DIが必要でBeanにする場合は、コンテナ側の登録だけを止めます。
@Bean
FilterRegistrationBean<ApiKeyFilter> apiKeyFilterRegistration(ApiKeyFilter filter) {
FilterRegistrationBean<ApiKeyFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
2本のフィルタにこの登録を足して同じリクエストを流すと、/api/helloでの呼び出しは各1回になり、/web/helloでは1回も呼ばれなくなりました。
Spring Bootの自動設定と静的リソースの扱い
Spring BootはFilterChainProxy自体をコンテナへ登録する部分も受け持ちます。この層を押さえると、CSSや画像が401になる問題や、他のServlet Filterとの前後関係を調整できます。
ignoringではなくpermitAllを使う理由
静的リソースを対象外にする方法としてWebSecurityCustomizerのignoringがよく使われますが、公式リファレンスの認可の章は「Favor permitAll over ignoring」という見出しで、これを避けるよう推奨しています。理由は「静的リソースであってもセキュアなヘッダを書き出すことは重要で、リクエストが無視されるとSpring Securityはそれを行えない」ためです。ignoringではチェーン内のフィルタがすべて飛ばされるため、HeaderWriterFilterのヘッダ付与も行われません(HttpFirewallはFilterChainProxyの入口で適用されるので残ります)。かつての「permitAllでもセッションを参照する」という性能上の欠点も、6以降は認可ルールが必要としない限りセッションを参照しなくなったため解消されています。
.authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.anyRequest().authenticated())
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と定義されています。値が小さいほど先に動くので、前章のとおり順序を指定しないBean由来のフィルタはSpring Securityの後になります。
spring.security.filter.dispatcher-typesの既定は全ディスパッチャタイプで、REQUESTに加えてFORWARD・INCLUDE・ERROR・ASYNCでもチェーンが動きます。設定キーは3系と同じですが、3系ではSecurityPropertiesの入れ子クラスFilterだったものが、4系では独立クラスSecurityFilterPropertiesへ移りました。SecurityPropertiesを直接参照していたコードは4系で書き換えが要ります。
ExceptionTranslationFilterによる認証・認可エラーの振り分け
後段のフィルタやコントローラから伝わってきた認証・認可の例外を、ログイン画面へのリダイレクト・401・403に変換するのがExceptionTranslationFilterです。手前のフィルタが自分で応答を書く場合(CSRFの拒否や、認証フィルタの失敗時の処理など)はここを通りません。
AuthenticationEntryPointとAccessDeniedHandlerの役割分担
このフィルタは後段のdoFilterをtryで囲み、AuthenticationExceptionとAccessDeniedExceptionを捕まえて次のように分岐します。
- 匿名、Remember-Meによる自動ログイン、または
AuthenticationExceptionの場合:SecurityContextHolderをクリアし、元のリクエストをRequestCacheへ保存したうえでAuthenticationEntryPointを呼びます。ログイン画面へのリダイレクトやWWW-Authenticateヘッダの送出がこれに当たります。 - それ以外の認証済みで
AccessDeniedExceptionの場合:AccessDeniedHandlerが403を返すか専用画面へ遷移させます。
未ログインのユーザーが管理画面を叩けばログイン画面へ、権限不足のログイン済みユーザーなら403へ、という挙動はこの分岐です。認可の失敗もメソッドセキュリティ(@PreAuthorizeなど)の失敗も、ここで1か所にまとめて応答へ変換されます。REST APIでリダイレクトさせたくないときは、複数チェーンの例のようにHttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)を指定します。
CSRFで拒否されたのに401が返る理由
既定構成のまま、正しいBasic認証情報を付けてCSRFトークンなしでPOSTすると、403ではなく401が返りました。TRACEログを追うと、経路は次のとおりです。
CsrfFilterがトークン不足で拒否し、403でエラーページへ回す/errorへのERRORディスパッチでもチェーンが動くBasicAuthenticationFilterはOncePerRequestFilterなので、ERRORディスパッチでは認証しない- 匿名のまま
AuthorizationFilterが/errorの認証を要求し、401になる
CsrfFilterはBasicAuthenticationFilterより前にあるため、拒否の時点ではまだ認証が済んでいません。本来の403を返すには、ERRORディスパッチを許可します。
.authorizeHttpRequests(auth -> auth
.dispatcherTypeMatchers(DispatcherType.ERROR).permitAll()
.anyRequest().authenticated())
この1行を足すと同じリクエストは403になり、本文に"status":403,"error":"Forbidden"が返りました。「APIで401が返るのに認証情報は正しい」というときは、CSRFや別のフィルタで拒否されたあとの/errorが原因になっていないかを先に確かめてください。
Spring Security 7とSpring Boot 4で置き換わったAPI
SecurityFilterChainの解説記事は5系から6系への移行期に書かれたものが多く、検索上位の記事にも、7.0で削除されたauthorizeRequests()やAntPathRequestMatcherが出てくるものがあります。サンプルを写す前に、まず使っているバージョンを確認します。
Spring BootとSpring Securityのバージョン対応
Spring Bootが引き込むSpring Securityの版は、spring-boot-dependencies 4.1.1のPOMなどにあるspring-security.versionが正本です。2026年10月14日時点でMaven Centralから確認した値です。サポート期限とGA予定はSpring Bootのサポート表(api.spring.ioの世代データ)によります。
| Spring Boot | Spring Security | 備考 |
|---|---|---|
| 3.5.16 | 6.5.11 | 3.5系のOSSサポートは2026年6月末で終了 |
| 4.0.8 | 7.0.7 | OSSサポートは2026年12月末まで |
| 4.1.1 | 7.1.1 | 現行GA |
| 4.2.0-M2 | 7.2.0-M2 | マイルストーン(GAは2026年11月予定) |
サポート期限と系列の選び方はSpring Bootバージョン一覧とサポート期限(EOL)に、4系の変更点全体はSpring Boot 4とは?最新バージョン4.1の変更点・新機能とSpring Boot 3との違いを解説にまとめています。
削除・非推奨になったAPIと代替
版帰属は、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 |
| 旧DSLのand() | 7.0で削除 | ラムダDSL |
| AntPathRequestMatcher | 6.5で非推奨・7.0で削除 | PathPatternRequestMatcher |
| FilterSecurityInterceptor | 7.0でaccessへ移動・非推奨 | AuthorizationFilter |
| AccessDecisionManager | 7.0でaccessへ移動・非推奨 | AuthorizationManager |
| SecurityContextPersistenceFilter | 非推奨(7.1.xにも存在) | SecurityContextHolderFilter |
FilterSecurityInterceptorとAccessDecisionManagerはクラスが消えたのではなく、7.0で新設されたspring-security-accessという別アーティファクトへ移されました。7.1.xのFilterOrderRegistrationでも、FilterSecurityInterceptorはクラス参照ではなく文字列で登録されています。spring-boot-starter-security 4.1.1が引き込むのはspring-security-configとspring-security-webだけなので、旧APIを使い続けるにはspring-security-accessを依存に足す必要があります。依存追加は移行猶予のための手段で、新規実装で選ぶものではありません。
変更は6.0と7.0の2回に分かれています。6.0で設定の書き方が、7.0で認可の実装とDSLの旧形式が変わりました。5系から7系へ一気に上げると両方が同時に来るため、公式は6.5を経由する段階的な移行を勧めています。SecurityFilterChain BeanをラムダDSLで書いているコードでも、AccessDecisionManagerやAntPathRequestMatcherを明示的に使っていた箇所は代表例にすぎません。7.0の変更はほかにもあるため、公式の7.0移行ガイドで使っている機能ごとに確認してください。実装レベルの移行はSpring Bootの認証実装:カスタムAuthenticationProviderの作り方とテスト【Spring Security 7.1対応】で扱っています。
よくある質問
Spring Bootの既定ではどのフィルタが有効になっていますか?
Spring Boot 4.1.1(Spring Security 7.1.1)で設定を書かずに起動すると、DisableEncodeUrlFilterからAuthorizationFilterまでの16本でした。版や有効化した機能で変わるため、logging.level.org.springframework.security=DEBUGを入れて起動し、Will secure any request with filters:に続く一覧を読むのが確実です。
addFilterBeforeとaddFilterAfterはどう使い分けますか?
基準フィルタの前か後かの違いだけです。選ぶべきは基準のほうで、APIキーやJWTの認証フィルタはLogoutFilterの後ろ、認証結果を読むフィルタはAnonymousAuthenticationFilterの後ろ、例外で拒否するフィルタはExceptionTranslationFilterの後ろに置きます。
SecurityFilterChainでCSRFを無効化しても問題ありませんか?
セッションCookieやHTTP Basicのように、ブラウザが認証情報を自動で送る経路では無効化しないでください。CSRF攻撃はこの自動送信を悪用するためで、セッションを持たないSTATELESS構成でも条件は同じです。無効化を検討してよいのは、JWTやAPIキーをアプリケーションが明示的にヘッダへ付けて送るAPIのように、ブラウザが自動で認証情報を送らない経路だけです。
Servlet標準のFilterChainとSecurityFilterChainは何が違いますか?
Servlet仕様のFilterChainはコンテナが管理する汎用の連鎖で、呼び出しはURLパターン・Servlet名・ディスパッチャタイプのマッピングで決まります。SecurityFilterChainはSpringのBeanで、FilterChainProxyの内側に構成され、RequestMatcherでHTTPメソッドやヘッダを含む条件で適用可否を判定できます。
自作フィルタが2回実行されるのはなぜですか?
フィルタを@Componentや@BeanでSpring Beanにしたうえで、HttpSecurityにも追加しているためです。コンテナ側の呼び出しはSpring Securityの後に、チェーンのsecurityMatcherと関係なく全URLで走ります。DIが不要ならBeanにせずnewで渡し、DIが要るならFilterRegistrationBeanのsetEnabled(false)でコンテナ登録だけを止めてください。
関連記事
- Spring Securityとは?認証・認可の仕組みと基本設定をわかりやすく解説【2026年版】
- Spring Bootの認証実装:カスタムAuthenticationProviderの作り方とテスト【Spring Security 7.1対応】
- Spring BootとReactの連携方法|REST API・CORS・JWT認証から1つのjarへの同梱まで【Spring Boot 4.1】
- Invalid CSRF tokenの意味と直し方|Laravel 419・Django・Spring Securityのエラーを切り分ける
- Spring Boot 4とは?最新バージョン4.1の変更点・新機能とSpring Boot 3との違いを解説【2026年最新】