GET は通るのに POST・PUT・DELETE だけ 403 になるときは、ほとんどが CSRF(クロスサイトリクエストフォージェリ対策)のトークンが付いていないのが原因です。 Spring Security は既定で、GET・HEAD・TRACE・OPTIONS 以外のリクエストに CSRF トークンを求めます。

原因を一番早く確かめる方法は、次の1行でログを出すことです。 Spring Security は、拒否した理由をレスポンスには書かず、ログにだけ書きます。

logging.level.org.springframework.security=TRACE
ログに出る文 原因 対処
Invalid CSRF token found for http://… CSRF トークンが無い・違う トークンを送る(SPA は csrf.spa())。Cookie を使わない API なら CSRF を外す
Pre-authenticated entry point called. Rejecting access ログインしていない(401 ではなく 403 になる設定) 認証方式を設定する。401 にしたいなら入口を変える
Denying request since did not find matching RequestMatcher どの requestMatchers にも当たらない 規則を足すか anyRequest() を置く
Checking authorization on POST /api/… using … の後に拒否 先に書いた規則に当たった(順番) 狭い規則を上に書く
Sending … to access denied handler since access is denied ログイン済みだが権限が無い hasRole と ROLE_ の対応を確かめる
Securing POST /error の後に拒否 本当のエラーが /error で隠れた /error か ERROR の転送を許可する
Invalid CORS request(本文) 許可していないオリジン CORS の設定にオリジンを足す

確認環境:Spring Boot 4.1.1(Spring Security 7.1.1、Spring Framework 7.0.9 は Boot の依存管理の版)。ログの文・例外の文・既定値は、GitHub の Spring Security 7.1.1 タグのソース(CsrfFilter・ExceptionTranslationFilter・Http403ForbiddenEntryPoint・RequestMatcherDelegatingAuthorizationManager・CsrfConfigurer など)で確かめました。設定の書き方とログの出力例は Spring Security 7.1 のリファレンス、7.0 での変更点は「What's New in Spring Security 7.0」に基づきます(確認日 2026年10月13日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring Securityで403になる原因|POSTだけ失敗する時」

まず 401 か 403 かを見る

状態 Boot の既定の設定のまま 自作の設定で認証方式が無い
ログインしていない ブラウザはログイン画面へ 302、curl などは 401 403
ログイン済み・権限が無い 403 403
CSRF トークンが無い・違う 403 403

「Boot の既定の設定のまま」は自分で SecurityFilterChain を作っていない状態、「自作の設定で認証方式が無い」は formLogin・httpBasic などを書いていない状態です。

ログインしていないのに 403 になるのは、認証の入口(AuthenticationEntryPoint)が決まっていないときです。 Spring Security 7.1.1 の ExceptionHandlingConfigurer は、formLogin・httpBasic・oauth2ResourceServer などを1つも設定していないと、Http403ForbiddenEntryPoint を使います。このクラスは次のログを出して 403 を返します。

DEBUG o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access

未ログインを 401 で返したい API では、入口を明示します。

http.exceptionHandling(e -> e
        .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)));

原因の切り分け表

症状 まず疑う原因 この記事の節
GET は通り、POST・PUT・DELETE だけ 403 CSRF 原因1
permitAll() を書いたのに 403 規則の順番・HTTP メソッドの指定・パスの書き方 原因2
400・404・500 のはずが 403(または 401) /error が保護されている 原因3
ログインしているのに 403 hasRole と ROLE_ の不一致 原因4
URL の規則は合っているのに 403 @PreAuthorize などのメソッドセキュリティ 原因5
ブラウザからだけ 403、curl では通る CORS のプリフライト 原因6
CSS・JS・画像が 403(またはログイン画面へ) 静的リソースが保護されている 原因7
2つ目の SecurityFilterChain の規則が効かない チェーンの順番 原因8

TRACE ログで確かめる

# application.yml(調べ終わったら戻す)
logging:
  level:
    org.springframework.security: TRACE

Spring Security 7.1 のリファレンスには、CSRF トークンを付けずに POST したときのログが載っています。

DEBUG ... o.s.security.web.FilterChainProxy        : Securing POST /hello
TRACE ... o.s.security.web.FilterChainProxy        : Invoking DisableEncodeUrlFilter (1/15)
TRACE ... o.s.security.web.FilterChainProxy        : Invoking WebAsyncManagerIntegrationFilter (2/15)
TRACE ... o.s.security.web.FilterChainProxy        : Invoking SecurityContextHolderFilter (3/15)
TRACE ... o.s.security.web.FilterChainProxy        : Invoking HeaderWriterFilter (4/15)
TRACE ... o.s.security.web.FilterChainProxy        : Invoking CsrfFilter (5/15)
DEBUG ... o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for http://localhost:8080/hello
DEBUG ... o.s.s.w.access.AccessDeniedHandlerImpl   : Responding with 403 status code

Invoking 〈フィルター名〉 (n/m) は「m 個のうち n 番目のフィルターを通った」という意味です。 最後に呼ばれたフィルターが、拒否した場所です。上の例では5番目の CsrfFilter で止まり、AuthorizationFilter(URL の規則を見るフィルター)まで届いていません。

7.1.1 のソースで、どのフィルターがどの文を出すかをまとめました。

ロガー(クラス) レベル 文 意味
FilterChainProxy DEBUG Securing POST /api/orders このリクエストの検査を始めた
FilterChainProxy DEBUG Secured POST /api/orders 全部通った(ここまで来れば Security は原因ではない)
CsrfFilter DEBUG Invalid CSRF token found for 〈URL〉 CSRF で拒否
CsrfFilter TRACE Did not protect against CSRF since request did not match … CSRF の検査対象外(GET など)
RequestMatcherDelegatingAuthorizationManager TRACE Authorizing POST /api/orders URL の規則を見始めた
同上 TRACE Checking authorization on POST /api/orders using 〈規則〉 この規則に当たった
同上 TRACE Denying request since did not find matching RequestMatcher どの規則にも当たらない
ExceptionTranslationFilter TRACE Sending 〈認証〉 to authentication entry point since access is denied 未ログイン扱い(入口へ)
ExceptionTranslationFilter TRACE Sending 〈認証〉 to access denied handler since access is denied ログイン済みで権限が無い
AccessDeniedHandlerImpl DEBUG Responding with 403 status code 403 を返した

Checking authorization on … using 〈規則〉 の 〈規則〉 を見ると、どの requestMatchers に当たったかが分かります。 意図と違う規則なら、原因2の順番の問題です。

TRACE はリクエストごとに十数行出ます。本番では INFO に戻します。ログの設定は関連記事「Spring Bootログ設定|logback-spring.xml全文と設定対応表」で扱っています。

原因1:CSRF(POST だけ 403)

Spring Security 7.1.1 の CsrfFilter は、次のメソッド以外のすべてに CSRF トークンを求めます。

// CsrfFilter.DefaultRequiresCsrfMatcher(7.1.1 のソース)
private final HashSet<String> allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));

permitAll() を付けても CSRF の検査は外れません。 CSRF は CsrfFilter、permitAll() は AuthorizationFilter の担当で、CsrfFilter が先に動くためです。「permitAll() が効かない、しかも POST だけ」というときは、ほぼこれです。

アプリの形 対処
Thymeleaf などのサーバー側のフォーム th:action を使えばトークンが自動で入る。手書きの <form> なら _csrf の hidden を足す
SPA(React・Vue・Angular)で Cookie のセッションを使う csrf.spa()(下の節)
JWT など、Cookie を使わないAPI CSRF を外す(csrf.disable())
外部からの Webhook だけ受けたい その URL だけ外す(ignoringRequestMatchers)

Cookie を使わない API なら外す

CSRF 攻撃は「ブラウザが Cookie を自動で送る」ことを悪用します。認証を Authorization: Bearer … ヘッダーだけで行い、Cookie を使わない API なら、CSRF の検査を外しても狙われる経路がありません。

@Bean
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .csrf(csrf -> csrf.disable())
        .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()));
    return http.build();
}

セッション Cookie でログインする画面があるなら、csrf.disable() にしないようにします。 その場合はトークンを送る形にします。JWT の設定の全文は関連記事「Spring Security JWT認証の実装|Boot 4対応の設定コード全文」で扱っています。

一部の URL だけ外す

http.csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/**"));

HTML フォームで送る

<form method="post" th:action="@{/orders}">
  <!-- th:action を使うと、次の hidden が自動で入る -->
  <!-- <input type="hidden" name="_csrf" value="…"> -->
  <button type="submit">注文する</button>
</form>

パラメータ名の既定は _csrf、ヘッダー名の既定は X-CSRF-TOKEN(セッションに保存する既定の HttpSessionCsrfTokenRepository の場合)です。

SPA から呼ぶときの CSRF(Security 7 の csrf.spa())

Spring Security 7.0 で、SPA 向けの設定が1行になりました。

http.csrf(csrf -> csrf.spa());

7.1.1 のソースでは、spa() は次の2つを設定します。

設定 中身 意味
トークンの保存先 CookieCsrfTokenRepository.withHttpOnlyFalse() Cookie XSRF-TOKEN に入れ、JavaScript から読めるようにする
トークンの受け取り方 SpaCsrfTokenRequestHandler(内部クラス) ヘッダーで来たら Cookie の値そのまま、フォームで来たら BREACH 対策の符号化された値として照合
名前 既定値(CookieCsrfTokenRepository)
Cookie 名 XSRF-TOKEN
ヘッダー名 X-XSRF-TOKEN
パラメータ名 _csrf

フロント側は、Cookie XSRF-TOKEN の値を、ヘッダー X-XSRF-TOKEN に入れて送ります。 axios と Angular の HttpClient は、この名前の組み合わせを既定で扱います(同じオリジンへのリクエストのとき)。fetch では自分で付けます。

function csrfToken() {
  const m = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/);
  return m ? decodeURIComponent(m[1]) : "";
}

await fetch("/api/orders", {
  method: "POST",
  credentials: "same-origin",
  headers: {
    "Content-Type": "application/json",
    "X-XSRF-TOKEN": csrfToken(),
  },
  body: JSON.stringify({ itemId: 1, qty: 2 }),
});

ログインとログアウトの後は、トークンが作り直されます。 リファレンスでは、CsrfAuthenticationStrategy と CsrfLogoutHandler が前のトークンを消すため、SPA は新しいトークンを受け取ってから POST する必要がある、と説明しています。spa() は毎回のリクエストでトークンを読み込む設定(setCsrfRequestAttributeName(null))を含むため、ログイン後に GET を1回すると新しい Cookie が届きます。

Security 6 系での書き方との違い

版 SPA 向けの書き方
Security 6.x CookieCsrfTokenRepository.withHttpOnlyFalse() と、自作の SpaCsrfTokenRequestHandler クラス、Cookie を毎回出すためのフィルターを自分で書く(6.x のリファレンスの例)
Security 7.0 以降 csrf.spa() の1行

Boot 4 に上げるときは、自作のクラスを消して spa() にまとめられます。CSRF と Cookie の SameSite の関係は、関連記事「CSRF対策とSameSite|Cookie送信をChromeで実測し多層防御を実装」で扱っています。

CSRF で 403 になるときの例外の文

7.1.1 のソースでは、403 の前に次のどちらかの例外が作られます(レスポンスの本文には出ません)。

例外 文 状況
MissingCsrfTokenException Could not verify the provided CSRF token because no token was found to compare. サーバー側にトークンが無い(セッション切れ・Cookie が届いていない)
InvalidCsrfTokenException Invalid CSRF Token '…' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'. 送られた値が違う(ログイン前のトークン・名前違い)

MissingCsrfTokenException なら、ブラウザが Cookie(セッションか XSRF-TOKEN)を送っていません。 別オリジンからの fetch で credentials: "include" が無い、Cookie の SameSite・Secure で落ちている、などを疑います。

原因2:permitAll が効かない(順番・メソッド・パス)

規則は上から順に見て、最初に当たったものが使われる

// NG:/api/public/** も 1行目に当たり、authenticated になる
http.authorizeHttpRequests(auth -> auth
        .requestMatchers("/api/**").authenticated()
        .requestMatchers("/api/public/**").permitAll()
        .anyRequest().denyAll());

// OK:狭い規則を上に
http.authorizeHttpRequests(auth -> auth
        .requestMatchers("/api/public/**").permitAll()
        .requestMatchers("/api/**").authenticated()
        .anyRequest().denyAll());

anyRequest() の後に requestMatchers を書くと、起動時に Can't configure requestMatchers after anyRequest で止まります(7.1.1 のソースの文)。

HTTP メソッドを指定している

.requestMatchers(HttpMethod.GET, "/api/items/**").permitAll()   // GET だけ
.anyRequest().authenticated()                                   // POST はこちら

メソッドを指定した permitAll() は、そのメソッドにしか効きません。 「GET は通るが POST は 403(または 401)」のもう1つの原因です。TRACE ログの Checking authorization on POST /api/items using … で、どの規則に当たったかを確かめます。

パスの書き方(Security 7)

書き方 当たるか(Security 7)
/api/users/** と /api/users 当たる(/** は0個以上の階層)
/api/users/* と /api/users/1/orders 当たらない(* は1階層だけ)
/api/**/orders 起動時のエラー('{*...}' or '**' pattern elements should be placed at the start or end of the pattern)
/**/orders と /api/v1/orders 当たる(Framework 7 の PathPattern は ** を先頭にも書ける)
/api/users と /api/users/ 当たらない(末尾のスラッシュは別のパス)

Spring Security 7.0 で AntPathRequestMatcher・MvcRequestMatcher は削除され、PathPatternRequestMatcher になりました(What's New in 7.0)。requestMatchers("…") に渡す文字列は PathPattern として解釈されます。パスにはコンテキストパス(server.servlet.context-path)を含めません。

原因3:/error が保護されている

Spring Security は、エラー画面への転送(ERROR ディスパッチ)も検査します。 permitAll() の URL で 400・404・500 が起きると、Spring Boot は /error へ転送します。/error が許可されていないと、未ログインの利用者には 401 か 403 が返り、本当の原因が隠れます。

本当に起きたこと 利用者に見える結果
permitAll() の URL で JSON の誤り(400) 401 または 403
存在しない URL(404) 401 または 403
コントローラーで例外(500) 401 または 403

TRACE ログでは、最初のリクエストの後に Securing POST /error が続きます。Spring Security のリファレンスでは、ERROR の転送を許可する書き方が紹介されています。

http.authorizeHttpRequests(auth -> auth
        .dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
        .requestMatchers("/api/public/**").permitAll()
        .anyRequest().authenticated());

requestMatchers("/error").permitAll() でも同じ効果があります。Spring Boot のリファレンスにも「/error も既定で保護される」と書かれています。400 の本当の原因(JSON の誤り)は関連記事「HttpMessageNotReadableExceptionの直し方|原因一覧」で扱っています。

原因4:hasRole と ROLE_ 接頭辞

hasRole("ADMIN") は、権限(GrantedAuthority)が ROLE_ADMIN かどうかを見ます。 7.1.1 の AuthorityAuthorizationManager.hasRole は、ROLE_ を自動で前に付けて hasAuthority を呼びます。

ユーザーの作り方・DB の値 実際の権限 hasRole("ADMIN") hasAuthority("ADMIN")
User.withUsername("a").roles("ADMIN") ROLE_ADMIN 通る 拒否
.authorities("ADMIN") ADMIN 拒否 通る
DB の値が ROLE_ADMIN で new SimpleGrantedAuthority(値) ROLE_ADMIN 通る 拒否
DB の値が ADMIN で new SimpleGrantedAuthority(値) ADMIN 拒否 通る

ROLE_ を付けたまま書くと、起動時に分かります(7.1.1 のソースの文)。

書き方 エラーの文
hasRole("ROLE_ADMIN") ROLE_ADMIN should not start with ROLE_ since ROLE_ is automatically prepended when using hasRole. Consider using hasAuthority instead.
User.withUsername("a").roles("ROLE_ADMIN") ROLE_ADMIN cannot start with ROLE_ (it is automatically added)

DB の値に ROLE_ を付けるか付けないかを決め、hasRole と hasAuthority のどちらで書くかを揃えます。 権限の取り違えは、TRACE ログの Sending UsernamePasswordAuthenticationToken [… Granted Authorities=[ADMIN]] to access denied handler の Granted Authorities で見分けられます。DB 認証の作り方は関連記事「Spring Security入門|SecurityFilterChainとDB認証」で扱っています。

原因5:メソッドセキュリティ(@PreAuthorize)

@EnableMethodSecurity
@Configuration
class MethodSecurityConfig {}

@RestController
class AdminController {
    @PreAuthorize("hasRole('ADMIN')")
    @DeleteMapping("/api/users/{id}")
    void delete(@PathVariable long id) { … }
}

URL の規則(authorizeHttpRequests)を通っても、メソッドの @PreAuthorize で拒否されると 403 になります。拒否されると AuthorizationDeniedException(文は Access Denied)が投げられます。

@ExceptionHandler(Exception.class) で全部の例外を受けていると、403 のはずが 500 になります。 AuthorizationDeniedException はコントローラーの中で投げられるため、先に @RestControllerAdvice に捕まります。受けたくない例外は投げ直すか、専用のハンドラーで 403 を返します。

@ExceptionHandler(AccessDeniedException.class)
ProblemDetail handleAccessDenied(AccessDeniedException ex) {
    return ProblemDetail.forStatusAndDetail(HttpStatus.FORBIDDEN, "この操作の権限がありません");
}

AccessDeniedException は org.springframework.security.access.AccessDeniedException です(AuthorizationDeniedException はその子クラス)。例外処理の全体は関連記事「Spring Boot例外処理|@ControllerAdvice完全版」で扱っています。

原因6:CORS のプリフライト

別のオリジン(http://localhost:5173 から http://localhost:8080 など)へ、Content-Type: application/json や独自ヘッダー付きで送ると、ブラウザは先に OPTIONS のリクエスト(プリフライト)を送ります。

プリフライトには Cookie や Authorization ヘッダーが付きません。 Spring Security のリファレンスには「CORS must be processed before Spring Security」とあり、CORS の処理を Security の前に置かないと、プリフライトが未ログインとして拒否されます。

@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
    http
        .cors(Customizer.withDefaults())
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .httpBasic(Customizer.withDefaults());
    return http.build();
}

@Bean
UrlBasedCorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("http://localhost:5173"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    config.setAllowedHeaders(List.of("Content-Type", "Authorization", "X-XSRF-TOKEN"));
    config.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
}
症状 原因
OPTIONS が 401・403 http.cors(…) が無く、プリフライトが認証で止まった
本文が Invalid CORS request で 403 オリジン・メソッド・ヘッダーが許可の一覧に無い(Spring Framework の DefaultCorsProcessor の文)
CORS は通るが POST が 403 別オリジンでも CSRF は効く。X-XSRF-TOKEN を allowedHeaders に入れ、トークンを送る

リファレンスには「Spring Security will automatically configure CORS only if a UrlBasedCorsConfigurationSource instance is present.」とあります。 戻り値の型を UrlBasedCorsConfigurationSource にして Bean を作ります。Spring Security 7.1 では、プリフライトを扱う PreFlightRequestFilter の対応も足されました(What's New in 7.1)。CORS の書き方の比較は関連記事「Spring Boot CORS設定|3つの書き方とSecurity併用のコード全文」で扱っています。開発中だけ CORS を避ける方法は関連記事「Angular proxy.conf.jsonの書き方|開発中のCORSを回避」で扱っています。

原因7:静的リソース

自分で SecurityFilterChain を作ると、/css/** や /js/** も anyRequest().authenticated() の対象になります。

import org.springframework.boot.security.autoconfigure.web.servlet.PathRequest;

http.authorizeHttpRequests(auth -> auth
        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
        .anyRequest().authenticated());

Boot 4.1.1 のソースで、atCommonLocations() が許可するパスは次のとおりです。

種類 パス
CSS /css/**
JavaScript /js/**
画像 /images/**
WebJars /webjars/**
アイコン /favicon.*・/icons/icon-*
フォント /fonts/**

PathRequest のパッケージは Boot 4 で変わりました。 Boot 3 の org.springframework.boot.autoconfigure.security.servlet.PathRequest は、Boot 4 では org.springframework.boot.security.autoconfigure.web.servlet.PathRequest です。/assets/** など別の場所に置いているなら、requestMatchers("/assets/**").permitAll() を足します。

原因8:SecurityFilterChain が2つ以上ある

@Bean @Order(1)
SecurityFilterChain api(HttpSecurity http) throws Exception {
    http.securityMatcher("/api/**") /* … */;
    return http.build();
}

@Bean @Order(2)
SecurityFilterChain web(HttpSecurity http) throws Exception {
    http /* securityMatcher なし=すべてのリクエスト */;
    return http.build();
}

リクエストは、最初に当たった1つのチェーンだけを通ります。 securityMatcher が無いチェーン(すべてに当たる)を先に置くと、後ろのチェーンは使われません。7.1.1 では起動時に … has already been configured, which means that this filter chain […] will never get invoked. Please use `HttpSecurity#securityMatcher` to ensure that there is only one filter chain configured for 'any request' and that the 'any request' filter chain is published last. で止まります。TRACE ログの Trying to match request against 〈チェーン〉 (n/m) で、どのチェーンに入ったかが分かります。

Spring Security 7 での主な変更点

「What's New in Spring Security 7.0」から、403 の調べ方に関わるものを抜き出しました。

変更 影響
authorizeRequests を削除し authorizeHttpRequests へ Security 5 系の例にある authorizeRequests() はコンパイルできない
HttpSecurity の DSL から and() を削除 ラムダで書く(http.csrf(c -> …))
MvcRequestMatcher・AntPathRequestMatcher を削除し PathPatternRequestMatcher へ /api/**/x のように ** を途中に書くと起動時のエラー
SPA 向けの CSRF 設定(csrf.spa())を追加 自作の CSRF ハンドラーが要らなくなった
AccessDecisionManager など Access API を spring-security-access へ移動 投票方式の設定を使うなら依存を足す

7.1 では、PreFlightRequestFilter の対応や、WWW-Authenticate ヘッダーへの charset の追加などが入っています(What's New in 7.1)。

テストで確かめる(MockMvc)

@WebMvcTest(OrderController.class)
@Import(SecurityConfig.class)
class OrderControllerSecurityTest {

    @Autowired MockMvc mvc;

    @Test
    @WithMockUser
    void CSRFトークンが無いPOSTは403() throws Exception {
        mvc.perform(post("/orders"))
            .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser
    void CSRFトークンがあれば通る() throws Exception {
        mvc.perform(post("/orders").with(csrf()))
            .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(roles = "USER")
    void ADMINでなければ削除できない() throws Exception {
        mvc.perform(delete("/api/users/1").with(csrf()))
            .andExpect(status().isForbidden());
    }
}

csrf() と @WithMockUser は spring-security-test の機能です(SecurityMockMvcRequestPostProcessors.csrf)。テストの書き方は関連記事「Spring Bootテスト入門|@WebMvcTestとMockMvcの使い分け」で扱っています。

調べる順番のチェックリスト

  1. logging.level.org.springframework.security=TRACE にして、もう一度送る
  2. Invalid CSRF token found があれば CSRF(原因1)
  3. Securing POST /error があれば、最初のリクエストの本当のエラーを探す(原因3)
  4. Checking authorization on … using 〈規則〉 で、当たった規則を確かめる(原因2)
  5. Granted Authorities=[…] と hasRole の対応を確かめる(原因4)
  6. Secured POST … まで出ているなら、Security ではなくコントローラー側(@PreAuthorize・例外ハンドラー)を見る(原因5)
  7. ブラウザだけで起きるなら、開発者ツールで OPTIONS の結果を見る(原因6)

よくある質問

Q. csrf.disable() にしてよいか迷います。 A. 判断の基準は「ブラウザが自動で送る認証情報(Cookie・Basic 認証)を使っているか」です。使っていないなら外しても狙われる経路がありません。セッション Cookie でログインする画面と API が同じアプリにあるなら、外さずにトークンを送る形にします。

Q. 403 の本文が空です。理由を返せませんか。 A. Spring Security は、攻撃者に手がかりを与えないよう、理由を本文に書きません。exceptionHandling(e -> e.accessDeniedHandler(…)) で自分のハンドラーを置くと本文を返せますが、CSRF の詳細など内部の理由は返さないようにします。調べるときはログを使います。

Q. Postman では通るのにブラウザでは 403 です。 A. CORS のプリフライト(原因6)か、CSRF の Cookie がブラウザで送られていない(Cookie の SameSite・Secure)ことを疑います。Postman はプリフライトを送らず、Cookie の扱いもブラウザと違います。

Q. ログインしているのに Pre-authenticated entry point called が出ます。 A. そのリクエストでログイン状態が引き継がれていません。セッション Cookie(JSESSIONID)が送られているか、SessionCreationPolicy.STATELESS にしていないかを確かめます。

まとめ

  • POST だけ 403 は CSRF。permitAll() では外れない
  • 調べるときは logging.level.org.springframework.security=TRACE。理由はログにだけ出る
  • 規則は上から順に最初に当たったもの。HTTP メソッドの指定にも注意
  • /error も保護される。400・404・500 が 401・403 に化ける
  • hasRole("ADMIN") は ROLE_ADMIN を見る
  • SPA は Security 7 の csrf.spa() と、Cookie XSRF-TOKEN → ヘッダー X-XSRF-TOKEN
  • formLogin・httpBasic が無い設定では、未ログインも 403 になる

参考・出典

確認日はいずれも 2026年10月13日です。

  • Spring Security リファレンス「Cross Site Request Forgery (CSRF)」:https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html
  • Spring Security リファレンス「Authorize HttpServletRequests」:https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html
  • Spring Security リファレンス「Architecture」(Logging):https://docs.spring.io/spring-security/reference/servlet/architecture.html
  • Spring Security リファレンス「CORS」:https://docs.spring.io/spring-security/reference/servlet/integrations/cors.html
  • What's New in Spring Security 7.0:https://docs.spring.io/spring-security/reference/7.0/whats-new.html
  • What's New in Spring Security 7.1:https://docs.spring.io/spring-security/reference/whats-new.html
  • Spring Boot リファレンス「Spring Security」:https://docs.spring.io/spring-boot/reference/web/spring-security.html
  • Spring Security ソースコード(7.1.1)CsrfFilter:https://github.com/spring-projects/spring-security/blob/7.1.1/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java
  • Spring Security ソースコード(7.1.1)CsrfConfigurer:https://github.com/spring-projects/spring-security/blob/7.1.1/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java
  • Spring Security ソースコード(7.1.1)ExceptionTranslationFilter:https://github.com/spring-projects/spring-security/blob/7.1.1/web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring Securityで403になる原因|POSTだけ失敗する時」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?