フロントエンドとバックエンドを分離したモダンなWebアプリケーション開発において、CORS(Cross-Origin Resource Sharing)の設定は避けて通れません。ReactやVueで構築したSPAから、Spring Bootで構築したREST APIにアクセスする際、適切なCORS設定がなければブラウザがリクエストをブロックしてしまいます。

この記事では、Spring Security環境でのCORS設定について、基本概念から本番環境向けのセキュアな設定まで体系的に解説します。@CrossOriginアノテーション、CorsConfigurationによるグローバル設定、CorsFilterの使い分けを理解し、プリフライトリクエストを正しく処理できるようになることを目指します。

実行環境と前提条件

本記事のコード例は以下の環境で動作を確認しています。

項目 バージョン・要件
Java 21
Spring Boot 3.4.x
Spring Security 6.4.x
ビルドツール Maven または Gradle

事前に以下の知識があると理解がスムーズです。

  • Spring Securityの基本的な設定方法
  • HTTPリクエスト/レスポンスの仕組み
  • REST APIの基礎知識

CORSの基本概念

CORSを適切に設定するために、まずその仕組みを理解しましょう。

同一オリジンポリシーとCORS

ブラウザは、セキュリティ上の理由から「同一オリジンポリシー」を適用しています。オリジンとは、プロトコル、ホスト、ポート番号の組み合わせです。

URL オリジン
https://app.example.com:443/page https://app.example.com
http://app.example.com:80/page http://app.example.com
https://api.example.com:443/users https://api.example.com

https://app.example.comからhttps://api.example.comへのリクエストは、ホスト名が異なるためクロスオリジンリクエストとなり、デフォルトではブラウザによってブロックされます。

CORSは、サーバーが適切なHTTPヘッダーを返すことで、特定のオリジンからのクロスオリジンリクエストを許可する仕組みです。

単純リクエストとプリフライトリクエスト

CORSには2種類のリクエストパターンがあります。

flowchart TB
    A[クロスオリジンリクエスト] --> B{単純リクエストの条件を満たす?}
    B -->|Yes| C[単純リクエスト<br/>直接リクエスト送信]
    B -->|No| D[プリフライトリクエスト<br/>OPTIONSで事前確認]
    
    C --> E[サーバーがCORSヘッダーを返す]
    D --> F{プリフライト成功?}
    F -->|Yes| G[実際のリクエストを送信]
    F -->|No| H[リクエストブロック]
    G --> E

単純リクエストの条件は以下のすべてを満たす場合です。

条件 許可される値
HTTPメソッド GET, HEAD, POST
Content-Type application/x-www-form-urlencoded, multipart/form-data, text/plain
カスタムヘッダー 使用しない(Accept, Accept-Language, Content-Language, Content-Typeのみ)

これらの条件を満たさない場合、ブラウザはまずOPTIONSメソッドでプリフライトリクエストを送信し、サーバーが許可するかどうかを確認します。

CORSレスポンスヘッダー

サーバーは以下のヘッダーを返すことでCORSを制御します。

ヘッダー 説明
Access-Control-Allow-Origin 許可するオリジン(*または具体的なオリジン)
Access-Control-Allow-Methods 許可するHTTPメソッド
Access-Control-Allow-Headers 許可するリクエストヘッダー
Access-Control-Allow-Credentials 認証情報(Cookie等)の送信を許可するか
Access-Control-Expose-Headers JavaScriptからアクセス可能にするレスポンスヘッダー
Access-Control-Max-Age プリフライト結果のキャッシュ時間(秒)

Spring SecurityとCORSの関係

Spring Securityを使用する場合、CORSの設定には特別な考慮が必要です。

CORSフィルターの実行順序

Spring Securityでは、CORSはセキュリティフィルターより前に処理される必要があります。これは、プリフライトリクエスト(OPTIONSメソッド)がCookieを含まないため、認証フィルターより後に処理されると認証エラーになってしまうからです。

flowchart LR
    A[HTTPリクエスト] --> B[CorsFilter]
    B --> C[SecurityFilterChain]
    C --> D[認証フィルター]
    D --> E[認可フィルター]
    E --> F[Controller]

Spring Securityは、CorsConfigurationSource Beanが存在する場合、自動的にCorsFilterを適切な位置に配置します。

Spring SecurityでCORSを有効化する基本設定

Spring Security 6.4では、HttpSecurity.cors()メソッドでCORSを有効化します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(csrf -> csrf.disable())  // REST APIの場合
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            );
        
        return http.build();
    }

    @Bean
    public UrlBasedCorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("https://app.example.com"));
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
        configuration.setMaxAge(3600L);

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

UrlBasedCorsConfigurationSourceをBeanとして定義すると、Spring Securityが自動的にCORS設定を検出して適用します。

CORS設定の3つの方法

Spring Bootでは、CORS設定に3つのアプローチがあります。用途に応じて使い分けましょう。

方法1: @CrossOriginアノテーション

コントローラーまたはメソッド単位でCORSを設定する最もシンプルな方法です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/users")
public class UserController {

    // メソッド単位での設定
    @CrossOrigin(origins = "https://app.example.com")
    @GetMapping
    public List<User> getAllUsers() {
        return userService.findAll();
    }

    // クラス単位での設定も可能
    @PostMapping
    public User createUser(@RequestBody User user) {
        return userService.save(user);
    }
}

クラスレベルで設定すると、すべてのメソッドに適用されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@CrossOrigin(
    origins = {"https://app.example.com", "https://staging.example.com"},
    methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE},
    allowedHeaders = {"Authorization", "Content-Type"},
    exposedHeaders = {"X-Custom-Header"},
    allowCredentials = "true",
    maxAge = 3600
)
@RestController
@RequestMapping("/api/products")
public class ProductController {
    // すべてのメソッドに上記のCORS設定が適用される
}

@CrossOriginの主なパラメータは以下のとおりです。

パラメータ 説明 デフォルト値
origins 許可するオリジン すべてのオリジン
methods 許可するHTTPメソッド マッピングで指定したメソッド
allowedHeaders 許可するリクエストヘッダー すべてのヘッダー
exposedHeaders クライアントに公開するレスポンスヘッダー なし
allowCredentials 認証情報の送信を許可 false
maxAge プリフライト結果のキャッシュ時間(秒) 1800(30分)

@CrossOriginアノテーションの適用場面は以下のとおりです。

  • 特定のエンドポイントのみCORSを許可したい場合
  • コントローラーごとに異なるCORS設定が必要な場合
  • シンプルなアプリケーションで素早くCORSを有効化したい場合

方法2: WebMvcConfigurerによるグローバル設定

アプリケーション全体で統一したCORS設定を行う場合は、WebMvcConfigurerを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://app.example.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
            .allowedHeaders("Authorization", "Content-Type", "X-Requested-With")
            .exposedHeaders("X-Total-Count", "X-Page-Number")
            .allowCredentials(true)
            .maxAge(3600);

        // 別のパスパターンに異なる設定を適用
        registry.addMapping("/public/**")
            .allowedOrigins("*")
            .allowedMethods("GET")
            .maxAge(86400);
    }
}

この方法はSpring MVCのCORS機能を使用します。Spring Securityを使用する場合、.cors(withDefaults())を指定すると、この設定が自動的に使用されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import static org.springframework.security.config.Customizer.withDefaults;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .cors(withDefaults())  // WebMvcConfigurerの設定を使用
        .csrf(csrf -> csrf.disable())
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    
    return http.build();
}

方法3: CorsConfigurationSourceによるSecurity統合設定

Spring Securityと密接に統合したCORS設定を行う場合は、CorsConfigurationSourceを使用します。この方法が最も柔軟で、本番環境で推奨されるアプローチです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;
import java.util.List;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            );
        
        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        
        // 許可するオリジン
        configuration.setAllowedOrigins(Arrays.asList(
            "https://app.example.com",
            "https://staging.example.com"
        ));
        
        // 許可するHTTPメソッド
        configuration.setAllowedMethods(Arrays.asList(
            "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"
        ));
        
        // 許可するリクエストヘッダー
        configuration.setAllowedHeaders(Arrays.asList(
            "Authorization",
            "Content-Type",
            "X-Requested-With",
            "Accept",
            "Origin"
        ));
        
        // クライアントに公開するレスポンスヘッダー
        configuration.setExposedHeaders(Arrays.asList(
            "X-Total-Count",
            "X-Page-Number",
            "X-Page-Size"
        ));
        
        // 認証情報(Cookie、Authorizationヘッダー)の送信を許可
        configuration.setAllowCredentials(true);
        
        // プリフライトリクエストのキャッシュ時間(1時間)
        configuration.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", configuration);
        return source;
    }
}

3つの方法の比較

項目 @CrossOrigin WebMvcConfigurer CorsConfigurationSource
設定範囲 メソッド/クラス単位 URLパターン単位 URLパターン単位
Spring Security統合 別途設定が必要 withDefaults()で統合 直接統合
動的設定 不可 不可 可能
推奨用途 プロトタイプ、小規模アプリ Spring MVC単体 Spring Security利用時

プリフライトリクエストの処理

PUT、DELETE、PATCHメソッドや、カスタムヘッダーを使用するリクエストでは、ブラウザがプリフライトリクエストを送信します。

プリフライトリクエストの流れ

sequenceDiagram
    participant Browser as ブラウザ
    participant Server as Spring Boot API

    Browser->>Server: OPTIONS /api/users (プリフライト)
    Note over Browser,Server: Origin: https://app.example.com<br/>Access-Control-Request-Method: PUT<br/>Access-Control-Request-Headers: Authorization, Content-Type

    Server-->>Browser: 200 OK
    Note over Browser,Server: Access-Control-Allow-Origin: https://app.example.com<br/>Access-Control-Allow-Methods: GET, POST, PUT, DELETE<br/>Access-Control-Allow-Headers: Authorization, Content-Type<br/>Access-Control-Max-Age: 3600

    Browser->>Server: PUT /api/users/1 (実際のリクエスト)
    Note over Browser,Server: Origin: https://app.example.com<br/>Authorization: Bearer xxx<br/>Content-Type: application/json

    Server-->>Browser: 200 OK
    Note over Browser,Server: Access-Control-Allow-Origin: https://app.example.com

プリフライトリクエストが発生する条件

以下のいずれかに該当する場合、プリフライトリクエストが発生します。

条件
単純リクエスト以外のHTTPメソッド PUT, DELETE, PATCH
許可されていないContent-Type application/json
カスタムヘッダーの使用 Authorization, X-Custom-Header

プリフライトリクエストの最適化

プリフライトリクエストは毎回送信されるとパフォーマンスに影響します。Access-Control-Max-Ageを設定することで、ブラウザにキャッシュさせることができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(List.of("https://app.example.com"));
    configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
    
    // プリフライト結果を1時間キャッシュ(ブラウザによって上限あり)
    configuration.setMaxAge(3600L);

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

主要ブラウザのキャッシュ上限は以下のとおりです。

ブラウザ 最大キャッシュ時間
Chrome 7200秒(2時間)
Firefox 86400秒(24時間)
Safari 7200秒(2時間)

認証情報付きリクエストの設定

JWTトークンやCookieを使用する認証では、追加の設定が必要です。

allowCredentialsの設定

認証情報を含むリクエストを許可するには、allowCredentialstrueに設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    
    // 重要: allowCredentials=true の場合、origins に "*" は使用できない
    configuration.setAllowedOrigins(List.of("https://app.example.com"));
    configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
    
    // 認証情報の送信を許可
    configuration.setAllowCredentials(true);

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

allowCredentialstrueにする場合の制約は以下のとおりです。

  • allowedOrigins"*"(ワイルドカード)は使用できない
  • 明示的なオリジンを指定するか、allowedOriginPatternsを使用する必要がある

allowedOriginPatternsの活用

動的なオリジンや複数のサブドメインを許可する場合は、allowedOriginPatternsを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    
    // パターンによるオリジン指定(allowCredentials=true でも使用可能)
    configuration.setAllowedOriginPatterns(List.of(
        "https://*.example.com",      // サブドメインを許可
        "https://app-*.example.com",  // 特定のプレフィックスを持つサブドメイン
        "http://localhost:[*]"        // ローカル開発用(任意のポート)
    ));
    
    configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
    configuration.setAllowCredentials(true);

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

フロントエンド側の設定

認証情報を送信するには、フロントエンド側でも設定が必要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// fetch API の場合
fetch('https://api.example.com/users', {
    method: 'GET',
    credentials: 'include',  // Cookie を含める
    headers: {
        'Authorization': 'Bearer ' + token,
        'Content-Type': 'application/json'
    }
});

// axios の場合
axios.get('https://api.example.com/users', {
    withCredentials: true,  // Cookie を含める
    headers: {
        'Authorization': 'Bearer ' + token
    }
});

複数のSecurityFilterChainでのCORS設定

異なるエンドポイントに異なるCORS設定を適用する場合、複数のSecurityFilterChainを定義します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@EnableWebSecurity
public class MultiCorsSecurityConfig {

    // API用のSecurityFilterChain(優先度高)
    @Bean
    @Order(1)
    public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .cors(cors -> cors.configurationSource(apiCorsConfigurationSource()))
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            );
        
        return http.build();
    }

    // 公開API用のSecurityFilterChain
    @Bean
    @Order(2)
    public SecurityFilterChain publicSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/public/**")
            .cors(cors -> cors.configurationSource(publicCorsConfigurationSource()))
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .anyRequest().permitAll()
            );
        
        return http.build();
    }

    // デフォルトのSecurityFilterChain
    @Bean
    @Order(3)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.disable())  // CORSを無効化
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            );
        
        return http.build();
    }

    // API用のCORS設定(厳格)
    private CorsConfigurationSource apiCorsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("https://app.example.com"));
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);

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

    // 公開API用のCORS設定(緩和)
    private CorsConfigurationSource publicCorsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("*"));  // すべてのオリジンを許可
        configuration.setAllowedMethods(List.of("GET"));  // GETのみ許可
        configuration.setMaxAge(86400L);

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

環境別CORS設定

開発環境と本番環境で異なるCORS設定を適用するパターンを紹介します。

プロファイルによる切り替え

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
public class CorsConfig {

    // 開発環境用
    @Bean
    @Profile("dev")
    public CorsConfigurationSource devCorsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(List.of("http://localhost:[*]"));
        configuration.setAllowedMethods(List.of("*"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);

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

    // 本番環境用
    @Bean
    @Profile("prod")
    public CorsConfigurationSource prodCorsConfigurationSource(
            @Value("${cors.allowed-origins}") List<String> allowedOrigins) {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(allowedOrigins);
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", configuration);
        return source;
    }
}

application.ymlでの設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# application-dev.yml
cors:
  allowed-origins:
    - http://localhost:3000
    - http://localhost:5173

# application-prod.yml
cors:
  allowed-origins:
    - https://app.example.com
    - https://www.example.com

設定クラスの外部化

より柔軟な設定のために、@ConfigurationPropertiesを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@ConfigurationProperties(prefix = "cors")
public class CorsProperties {
    
    private List<String> allowedOrigins;
    private List<String> allowedMethods;
    private List<String> allowedHeaders;
    private List<String> exposedHeaders;
    private Boolean allowCredentials;
    private Long maxAge;

    // getter/setter
    public List<String> getAllowedOrigins() {
        return allowedOrigins;
    }

    public void setAllowedOrigins(List<String> allowedOrigins) {
        this.allowedOrigins = allowedOrigins;
    }

    public List<String> getAllowedMethods() {
        return allowedMethods;
    }

    public void setAllowedMethods(List<String> allowedMethods) {
        this.allowedMethods = allowedMethods;
    }

    public List<String> getAllowedHeaders() {
        return allowedHeaders;
    }

    public void setAllowedHeaders(List<String> allowedHeaders) {
        this.allowedHeaders = allowedHeaders;
    }

    public List<String> getExposedHeaders() {
        return exposedHeaders;
    }

    public void setExposedHeaders(List<String> exposedHeaders) {
        this.exposedHeaders = exposedHeaders;
    }

    public Boolean getAllowCredentials() {
        return allowCredentials;
    }

    public void setAllowCredentials(Boolean allowCredentials) {
        this.allowCredentials = allowCredentials;
    }

    public Long getMaxAge() {
        return maxAge;
    }

    public void setMaxAge(Long maxAge) {
        this.maxAge = maxAge;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
public class CorsConfig {

    private final CorsProperties corsProperties;

    public CorsConfig(CorsProperties corsProperties) {
        this.corsProperties = corsProperties;
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(corsProperties.getAllowedOrigins());
        configuration.setAllowedMethods(corsProperties.getAllowedMethods());
        configuration.setAllowedHeaders(corsProperties.getAllowedHeaders());
        configuration.setExposedHeaders(corsProperties.getExposedHeaders());
        configuration.setAllowCredentials(corsProperties.getAllowCredentials());
        configuration.setMaxAge(corsProperties.getMaxAge());

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", configuration);
        return source;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# application.yml
cors:
  allowed-origins:
    - https://app.example.com
  allowed-methods:
    - GET
    - POST
    - PUT
    - DELETE
  allowed-headers:
    - Authorization
    - Content-Type
  exposed-headers:
    - X-Total-Count
  allow-credentials: true
  max-age: 3600

本番環境向けの安全なCORS設定

本番環境では、セキュリティを考慮したCORS設定が不可欠です。

設定のベストプラクティス

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Bean
public CorsConfigurationSource secureCorsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    
    // 1. 許可するオリジンを明示的に指定("*" は使用しない)
    configuration.setAllowedOrigins(List.of(
        "https://app.example.com",
        "https://admin.example.com"
    ));
    
    // 2. 必要なHTTPメソッドのみ許可
    configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    
    // 3. 必要なヘッダーのみ許可
    configuration.setAllowedHeaders(List.of(
        "Authorization",
        "Content-Type",
        "X-Requested-With"
    ));
    
    // 4. 公開するレスポンスヘッダーを最小限に
    configuration.setExposedHeaders(List.of("X-Request-Id"));
    
    // 5. 認証情報の送信は必要な場合のみ許可
    configuration.setAllowCredentials(true);
    
    // 6. プリフライトキャッシュを適切に設定
    configuration.setMaxAge(3600L);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", configuration);
    return source;
}

避けるべき設定

以下の設定はセキュリティリスクがあるため、本番環境では避けてください。

1
2
3
4
5
6
7
// 悪い例: すべてを許可する設定
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("*"));      // すべてのオリジンを許可
configuration.setAllowedMethods(List.of("*"));      // すべてのメソッドを許可
configuration.setAllowedHeaders(List.of("*"));      // すべてのヘッダーを許可
configuration.setAllowCredentials(true);            // 認証情報も許可
// この組み合わせは動作しない(allowCredentials=true と origins="*" は併用不可)

セキュリティ考慮事項

項目 推奨設定 理由
allowedOrigins 明示的なオリジンのリスト 不正なオリジンからのアクセスを防止
allowedMethods 必要なメソッドのみ 不要な操作を制限
allowedHeaders 必要なヘッダーのみ ヘッダーインジェクションのリスク軽減
allowCredentials 必要な場合のみtrue CSRF攻撃のリスク軽減
exposedHeaders 必要なヘッダーのみ 情報漏洩の防止

CORS設定のテスト

CORS設定が正しく動作しているかテストする方法を紹介します。

MockMvcを使用した単体テスト

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class CorsConfigurationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void プリフライトリクエストが正しく処理される() throws Exception {
        mockMvc.perform(options("/api/users")
                .header("Origin", "https://app.example.com")
                .header("Access-Control-Request-Method", "PUT")
                .header("Access-Control-Request-Headers", "Authorization, Content-Type"))
            .andExpect(status().isOk())
            .andExpect(header().string("Access-Control-Allow-Origin", "https://app.example.com"))
            .andExpect(header().exists("Access-Control-Allow-Methods"))
            .andExpect(header().exists("Access-Control-Allow-Headers"))
            .andExpect(header().exists("Access-Control-Max-Age"));
    }

    @Test
    void 許可されたオリジンからのリクエストが成功する() throws Exception {
        mockMvc.perform(get("/api/users")
                .header("Origin", "https://app.example.com"))
            .andExpect(status().isOk())
            .andExpect(header().string("Access-Control-Allow-Origin", "https://app.example.com"));
    }

    @Test
    void 許可されていないオリジンからのリクエストはCORSヘッダーが返されない() throws Exception {
        mockMvc.perform(get("/api/users")
                .header("Origin", "https://malicious.example.com"))
            .andExpect(header().doesNotExist("Access-Control-Allow-Origin"));
    }

    @Test
    void 認証情報付きリクエストでAllowCredentialsヘッダーが返される() throws Exception {
        mockMvc.perform(get("/api/users")
                .header("Origin", "https://app.example.com"))
            .andExpect(header().string("Access-Control-Allow-Credentials", "true"));
    }
}

curlを使用した動作確認

プリフライトリクエストのテストは以下のコマンドで実行できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# プリフライトリクエスト
curl -X OPTIONS http://localhost:8080/api/users \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: PUT" \
  -H "Access-Control-Request-Headers: Authorization, Content-Type" \
  -v

# 実際のリクエスト
curl -X GET http://localhost:8080/api/users \
  -H "Origin: https://app.example.com" \
  -v

期待されるレスポンスヘッダーは以下のとおりです。

1
2
3
4
5
6
< HTTP/1.1 200
< Access-Control-Allow-Origin: https://app.example.com
< Access-Control-Allow-Methods: GET, POST, PUT, DELETE
< Access-Control-Allow-Headers: Authorization, Content-Type
< Access-Control-Allow-Credentials: true
< Access-Control-Max-Age: 3600

トラブルシューティング

CORSに関するよくある問題と解決方法を紹介します。

よくあるエラーと解決方法

エラー 原因 解決方法
No ‘Access-Control-Allow-Origin’ header CORS設定が適用されていない CorsConfigurationSourceがBeanとして登録されているか確認
CORS policy: Response to preflight request doesn’t pass OPTIONSリクエストが認証で拒否 SecurityFilterChainでCORSを有効化
The value of ‘Access-Control-Allow-Origin’ must not be ‘*’ when credentials mode is ‘include’ allowCredentials=trueでワイルドカード使用 明示的なオリジンを指定
Method not allowed by Access-Control-Allow-Methods 許可されていないメソッド allowedMethodsに追加

デバッグログの有効化

CORS処理のデバッグログを有効にするには、application.ymlに以下を追加します。

1
2
3
4
logging:
  level:
    org.springframework.web.cors: DEBUG
    org.springframework.security.web: DEBUG

Spring Security Filter Chainの確認

現在適用されているフィルターを確認するには、以下のBeanを登録します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;

import jakarta.servlet.Filter;
import java.util.List;

@Configuration
public class SecurityDebugConfig {

    @Bean
    public CommandLineRunner securityFilterChainDebugger(FilterChainProxy filterChainProxy) {
        return args -> {
            List<SecurityFilterChain> filterChains = filterChainProxy.getFilterChains();
            for (SecurityFilterChain chain : filterChains) {
                System.out.println("SecurityFilterChain: " + chain);
                for (Filter filter : chain.getFilters()) {
                    System.out.println("  - " + filter.getClass().getSimpleName());
                }
            }
        };
    }
}

CorsFilterSecurityFilterChainの上位に配置されていることを確認してください。

まとめ

本記事では、Spring SecurityでのCORS設定について解説しました。

項目 内容
CORS設定方法 @CrossOrigin、WebMvcConfigurer、CorsConfigurationSourceの3つ
推奨アプローチ Spring Security利用時はCorsConfigurationSourceを使用
プリフライト処理 OPTIONSメソッドへの適切な応答とキャッシュ設定
認証情報の取り扱い allowCredentials=trueの場合は明示的なオリジン指定が必須
環境別設定 プロファイルまたはConfigurationPropertiesで切り替え
セキュリティ 本番環境では必要最小限の設定を心がける

フロントエンドとバックエンドを分離した構成では、CORS設定は必須の要素です。Spring Securityと適切に統合し、セキュアなAPI開発を進めてください。

参考リンク