---
title: "SecurityFilterChainとは？Spring Securityのフィルタ実行順序と設定例【Spring Security 7.1対応】"
url: "https://www.issoh.co.jp/tech/details/7001/"
published: 2025-05-28
updated: 2026-10-14
categories: ["Spring"]
publisher: "株式会社一創"
---

# SecurityFilterChainとは？Spring Securityのフィルタ実行順序と設定例【Spring Security 7.1対応】

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公式リファレンスのアーキテクチャ解説](https://docs.spring.io/spring-security/reference/servlet/architecture.html)は、これを「Servletコンテナのライフサイクルと`ApplicationContext`の間を橋渡しするFilter実装」と説明しています。コンテナ標準の登録機構を使いながら、実処理はBeanとして定義されたフィルタへ委譲します。Bean名で遅延取得するため、コンテキストの初期化前にコンテナがフィルタを要求しても破綻しません。委譲先が`FilterChainProxy`で、その内側に複数のSecurityFilterChainがぶら下がる三層構造です。

### FilterChainProxyを経由させる3つの実利

- **トラブルシュートの起点になる**：Servlet向けの処理はすべてここから始まるため、挙動を追うときは`FilterChainProxy`にブレークポイントを置けば足ります。
- **メモリリークを防ぐ**：リクエスト処理の終了時に`SecurityContext`をクリアします。
- **照合の条件を広げられる**：コンテナのフィルタ登録はURLパターン・Servlet名・ディスパッチャタイプで呼び出しを決めますが、`FilterChainProxy`は`RequestMatcher`でHTTPメソッドやヘッダまで判定に使えます。

パスの正規化を悪用した攻撃を弾く`HttpFirewall`もここで適用されます。Spring Security全体の位置づけは[Spring Securityとは？認証・認可の仕組みと基本設定をわかりやすく解説](/tech/details/2465/)で扱っています。

### 最初に一致した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）](https://github.com/spring-projects/spring-security/blob/7.1.x/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java)です。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が自動生成します。

### authorizeHttpRequestsとrequestMatchersによるURL単位の認可

```
@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のエラーを切り分ける](/tech/details/3031/)で扱っています。

```
@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対応】](/tech/details/1931/)で扱っています。

## カスタムフィルタを追加する位置と手順

独自の認証や監査ログを差し込むときは、フィルタを自作してチェーンに登録します。置く位置を間違えても起動は成功するので、ログと応答コードで確かめるところまでが手順です。

### 位置を決める基準と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管理システム開発](/service/system/identity/)の支援内容も判断材料になります。JWTを検証するフィルタも同じ位置に入ります。SPAとJWTの組み合わせは[Spring BootとReactの連携方法｜REST API・CORS・JWT認証から1つのjarへの同梱まで【Spring Boot 4.1】](/tech/details/1542/)で扱っています。

### 認証後フィルタによる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](https://github.com/spring-projects/spring-security/blob/7.1.x/docs/src/test/java/org/springframework/security/docs/servlet/customizingfilter/CustomizingFilterTests.java)も、この誤った構成で「両方のフィルタが存在する」ことを確かめる内容です。**`addFilterAt`は置き換えではなく同じ位置への追加**なので、標準フィルタを差し替えるときは、対応するDSL（`httpBasic`など）を書かないか`disable()`してから使います。

## フィルタのBean登録が招く二重実行と主体のすり替わり

自作フィルタに`@Component`を付けたり`@Bean`で定義したりすると、Spring Bootはそれを組込コンテナにも登録します。[Spring Bootリファレンス](https://docs.spring.io/spring-boot/reference/web/servlet.html)は「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`がよく使われますが、[公式リファレンスの認可の章](https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html)は「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ログを追うと、経路は次のとおりです。

1. `CsrfFilter`がトークン不足で拒否し、403でエラーページへ回す
2. `/error`へのERRORディスパッチでもチェーンが動く
3. `BasicAuthenticationFilter`は`OncePerRequestFilter`なので、ERRORディスパッチでは認証しない
4. 匿名のまま`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](https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-dependencies/4.1.1/spring-boot-dependencies-4.1.1.pom)などにある`spring-security.version`が正本です。2026年10月14日時点でMaven Centralから確認した値です。サポート期限とGA予定は[Spring Bootのサポート表](https://spring.io/projects/spring-boot#support)（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）](/tech/details/5172/)に、4系の変更点全体は[Spring Boot 4とは？最新バージョン4.1の変更点・新機能とSpring Boot 3との違いを解説](/tech/details/10124/)にまとめています。

### 削除・非推奨になったAPIと代替

版帰属は、[spring-projects/spring-security](https://github.com/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移行ガイド](https://docs.spring.io/spring-security/reference/6.5/migration-7/index.html)で使っている機能ごとに確認してください。実装レベルの移行は[Spring Bootの認証実装：カスタムAuthenticationProviderの作り方とテスト【Spring Security 7.1対応】](/tech/details/2467/)で扱っています。

## よくある質問

### 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年版】](/tech/details/2465/)
- [Spring Bootの認証実装：カスタムAuthenticationProviderの作り方とテスト【Spring Security 7.1対応】](/tech/details/2467/)
- [Spring BootとReactの連携方法｜REST API・CORS・JWT認証から1つのjarへの同梱まで【Spring Boot 4.1】](/tech/details/1542/)
- [Invalid CSRF tokenの意味と直し方｜Laravel 419・Django・Spring Securityのエラーを切り分ける](/tech/details/3031/)
- [Spring Boot 4とは？最新バージョン4.1の変更点・新機能とSpring Boot 3との違いを解説【2026年最新】](/tech/details/10124/)

---

出典: [SecurityFilterChainとは？Spring Securityのフィルタ実行順序と設定例【Spring Security 7.1対応】](<https://www.issoh.co.jp/tech/details/7001/>)（株式会社一創）
