【Spring Boot 3 / Security 6】JWTで守るAPIサーバー最小構成:SecurityFilterChain・CORS・CSRF・401/403まで全部つなげる

JWTでAPIを守るとき、最小構成の考え方はシンプルです

  • 認証方式:Authorization: Bearer <JWT> を検証する(Resource Server)
  • セッション:使わない(STATELESS
  • CSRF:基本的に「ブラウザフォーム+セッション」向けの仕組みなので、ステートレスAPIではOFFにすることが多い(ただし要件次第)
  • CORS:SPA/フロントから呼ぶなら必須(preflightのOPTIONSに注意)

まずこれだけ入れる(依存関係)

JWTを検証するResource Serverとして動かすなら、少なくとも以下が必要です(BootならstarterでOK)

  • spring-boot-starter-security
  • spring-boot-starter-oauth2-resource-server

この構成はSpring Security側のユースケースとして明記されています


application.yml:issuer-uri(基本)+ audiences(必要なら)

JWT検証の入り口はissuer-uriが定番です
Resource Serverは起動時にメタデータからJWKs URL等を解決し、署名検証に必要な鍵情報を取得します

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com
          # audiences: https://my-resource-server.example.com  # aud検証したい場合

audiencesaudクレームを検証する設定も用意されています


【コピペOK】SecurityFilterChain:JWT + ステートレス + CORS + CSRF(最小)

Spring Security 6(Boot 3)ではSecurityFilterChainをBean定義して組み立てるのが基本です

package com.example.security;

import java.util.List;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      // 1) API中心ならステートレス
      .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

      // 2) ブラウザから呼ぶならCORSは必須(下にCorsConfigurationSourceを用意)
      .cors(Customizer.withDefaults())

      // 3) ステートレスAPIではCSRFを無効化することが多い(要件により)
      .csrf(csrf -> csrf.disable())

      // 4) 認可ルール
      .authorizeHttpRequests(auth -> auth
        // ヘルスチェックやSwagger等は公開しがち(要件に合わせて調整)
        .requestMatchers("/actuator/health", "/swagger-ui/**", "/v3/api-docs/**").permitAll()

        // preflight(OPTIONS)は通す(CORSで詰まる人が多い)
        .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()

        // それ以外はJWT必須
        .anyRequest().authenticated()
      )

      // 5) JWT(Bearer Token)を検証して認証する(Resource Server)
      .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));

    return http.build();
  }

  @Bean
  CorsConfigurationSource corsConfigurationSource() {
    var config = new CorsConfiguration();

    // 例:ローカル開発のフロント
    config.setAllowedOrigins(List.of("http://localhost:3000"));

    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
    config.setAllowedHeaders(List.of("*"));
    config.setAllowCredentials(true);

    var source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
  }
}

401/403/CORSで詰まったら、まずここを疑う(原因トップ4)

401(Unauthorized):そもそもJWTが届いてない

フロントがAuthorizationヘッダを送っていない/送れていない

  • APIクライアント(Postman/curl)では通るのに、ブラウザからだけ401 → CORSでヘッダが落ちてる可能性

チェック:ブラウザのNetworkタブで Authorization: Bearer ... が載っているか確認

401:issuer-uriやJWKs解決で失敗してる

issuer-uriを指定すると、Resource Serverはメタデータを辿ってJWKs URLなどを決定し、検証戦略を構成します
ここがズレると起動時/リクエスト時に検証が失敗します

対処

  • issuerのURL(末尾スラッシュなど)をIDP側の発行値に合わせる
  • IDPのメタデータ公開(.well-known)が有効か確認

403(Forbidden):CSRFが効いている(APIなのに)

GETは通るがPOST/PUT/DELETEで403
ステートレスAPIなら csrf.disable() が必要になることが多いです(ただし要件次第)

CORS:preflightのOPTIONSが弾かれている

ブラウザからだけ失敗(コンソールにCORSエラー)

対処

  • http.cors() を有効にする
  • OPTIONS /**permitAll() しておく
  • allowedOrigins / allowedHeaders をフロントに合わせる

役割(ROLE)や権限(SCOPE)で認可したい場合の最短例

JWTを使うと「このAPIはログイン必須」だけでなく、「管理者だけ」や「特定スコープだけ」にしたくなります

例(スコープに応じてアクセス制御したいイメージ):

.authorizeHttpRequests(auth -> auth
  .requestMatchers("/admin/**").hasAuthority("SCOPE_admin")
  .requestMatchers("/api/**").hasAnyAuthority("SCOPE_read", "SCOPE_write")
  .anyRequest().authenticated()
)

※ どのクレームを GrantedAuthority に変換するかはIDPやトークン設計次第です。まずは JWTが認証されてPrincipalが作れるところまでを最短で通し、次に変換(Converter)へ進むのが事故りにくいです
(JWT Resource Serverの基本フローは公式が最も正確です)


開発中だけ「認証なし」にしたい(localプロファイル切替)

ローカルで詰まり続けると開発速度が落ちます。プロファイルでSecurityFilterChainを切り替えるのが安全です

@Bean
@Profile("local")
SecurityFilterChain localSecurity(HttpSecurity http) throws Exception {
  http
    .csrf(csrf -> csrf.disable())
    .authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
  return http.build();
}


依存を入れるだけでデフォルトのセキュリティが有効になり、デフォルトユーザーやパスワードの挙動もあります(開発向け)

ただし、JWT(Resource Server)で運用するなら、この記事のように 自分のアプリ要件に合わせてSecurityFilterChainを明示していくのが前提になります

ぜひ、ご参考くださいっ!

是非フォローしてください

最新の情報をお伝えします

類似投稿