フロントエンドとバックエンドを分離したモダンな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を有効化したい場合
アプリケーション全体で統一した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の設定#
認証情報を含むリクエストを許可するには、allowCredentialsをtrueに設定します。
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;
}
|
allowCredentialsをtrueにする場合の制約は以下のとおりです。
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());
}
}
};
}
}
|
CorsFilterがSecurityFilterChainの上位に配置されていることを確認してください。
まとめ#
本記事では、Spring SecurityでのCORS設定について解説しました。
| 項目 |
内容 |
| CORS設定方法 |
@CrossOrigin、WebMvcConfigurer、CorsConfigurationSourceの3つ |
| 推奨アプローチ |
Spring Security利用時はCorsConfigurationSourceを使用 |
| プリフライト処理 |
OPTIONSメソッドへの適切な応答とキャッシュ設定 |
| 認証情報の取り扱い |
allowCredentials=trueの場合は明示的なオリジン指定が必須 |
| 環境別設定 |
プロファイルまたはConfigurationPropertiesで切り替え |
| セキュリティ |
本番環境では必要最小限の設定を心がける |
フロントエンドとバックエンドを分離した構成では、CORS設定は必須の要素です。Spring Securityと適切に統合し、セキュアなAPI開発を進めてください。
参考リンク#