ReactやVueでフロントエンドを作ってSpring Boot REST APIと繋ごうとしたとき、必ずと言っていいほど遭遇するのがCORSエラーですよね。
Access to XMLHttpRequest at 'http://localhost:8080/api/users'
from origin 'http://localhost:3000' has been blocked by CORS policy
「@CrossOrigin を付けたのに効かない」「Spring Securityを入れたら突然設定が無視された」という経験、ありませんか。この記事では3つの設定パターンを順番に解説しつつ、Spring Security導入時の落とし穴まで押さえます。
CORSとは - ブラウザがエラーを出す仕組み
CORSとは Cross-Origin Resource Sharing の略で、ブラウザのセキュリティ機能である 同一オリジンポリシー を安全に緩和する仕組みです。
オリジンとは プロトコル + ホスト + ポート の組み合わせのこと。http://localhost:3000 と http://localhost:8080 はポートが違うので 別オリジン になります。
ブラウザはPOSTやカスタムヘッダーを含むリクエストを送る前に、OPTIONSメソッドで Preflightリクエスト を投げます。サーバーが Access-Control-Allow-Origin などのヘッダーを返さないと、ブラウザ側でブロックされます。
curlやPostmanでは同じリクエストが成功するのに、ブラウザだけエラーになるのはこのためです。サーバーが正しいレスポンスヘッダーを返すよう設定するのがCORS対応です。
パターン1: @CrossOriginアノテーション
最もシンプルな方法です。コントローラーに直接付けるだけで動きます。
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {
@GetMapping
public List<User> getUsers() { ... }
// このエンドポイントだけ別のオリジンを許可したい場合
@PostMapping
@CrossOrigin(origins = "https://app.example.com")
public User createUser(@RequestBody UserRequest req) { ... }
}
手軽ですが、エンドポイントが増えるたびに付け忘れが発生しやすいです。Spring Securityなし・試作段階・特定エンドポイントだけ許可したいケース向けの選択肢と考えてください。
パターン2: WebMvcConfigurerでグローバル設定
全エンドポイントに一括でCORSを適用するなら WebMvcConfigurer を使います。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
addMapping("/**") でアプリ全体に適用できます。パスごとに設定を分けたい場合は addMapping("/api/**") のように書けます。
Spring Securityを使っていない環境ではこれで完璧です。ただし、Spring Securityを導入すると突然この設定が効かなくなります。
Spring Securityを導入するとCORS設定が効かなくなる理由
これが多くの人がハマるポイントです。
Spring SecurityはDispatcherServletよりも 前段のFilter層 で動作します。WebMvcConfigurer のCORS設定はDispatcherServlet以降のSpringMVC層に適用されるため、SecurityFilterChainがリクエストを先に捕まえてしまうのです。
PreflightのOPTIONSリクエストが来た際、SecurityFilterChainが「認証されていないリクエスト」として 401を返してしまい 、ブラウザがCORSエラーと認識します。WebMvcConfigurerの設定にはそもそも到達すらしていません。
解決策はSecurityFilterChain側にもCORSを認識させることです。
パターン3: SecurityFilterChainでCORSを設定する
Spring Security 6.x対応の書き方です。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
CorsConfigurationSource をBeanとして定義しておくと、将来 WebMvcConfigurer と設定を共有することもできて管理しやすいです。Spring Security 5.x(WebSecurityConfigurerAdapter)を使っている場合は書き方が異なりますが、Spring Boot 3.x環境では上記の形式が標準です。
allowedOriginsとallowedOriginPatternsの違い
本番環境でよく問題になる点です。
allowCredentials(true) にしているのに allowedOrigins("*") を指定すると、Springがエラーを投げます。CORSの仕様上、Credentialsを含むリクエストではワイルドカード指定が禁止されているためです。
代わりに allowedOriginPatterns を使うとワイルドカードが使えます。
// allowCredentials(true)と組み合わせて使える
config.setAllowedOriginPatterns(List.of("https://*.example.com"));
// 開発環境では * でも可
config.setAllowedOriginPatterns(List.of("*"));
本番環境では具体的なオリジンを明示するのが安全です。環境ごとに切り替えたい場合は Spring Profilesを使った設定切り替え が参考になります。
パターン4: CorsFilterをBeanとして登録する
実は、Spring MVCの設定ともSecurityFilterChainとも別に、CorsFilter をBeanとして登録する方法もあります。
@Configuration
public class CorsFilterConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
Spring Bootは Filter 型のBeanを見つけると自動でサーブレットFilterとして登録するので、これだけでアプリ全体にグローバルなCORS設定が効きます。DispatcherServletより前の段階でPreflightに応答するため、Spring MVC以外のサーブレットが同居している構成でも使えるのが利点です。
ただし、Spring Securityと併用する場合は注意が必要です。Bean登録した CorsFilter の実行順は既定で最も低い優先度になるため、SecurityFilterChainより後ろに並びます。つまりこの方法でも、パターン3と同じくSecurityFilterChain側で cors() を有効にしておかないと、PreflightがSecurityFilterChainで止まってしまいます。Spring Securityは corsFilter という名前の CorsFilter Beanを見つけると、cors() の中でそのFilterをそのまま使ってくれます。
結論として、Spring Securityを使うなら素直にパターン3、使わないならWebMvcConfigurerかこの CorsFilter Beanのどちらか一つ、という使い分けで十分です。両方書いても壊れはしませんが、設定が二重管理になるので一つに絞りましょう。
cors(Customizer.withDefaults())でCorsConfigurationSource Beanを自動検出する
パターン3では cors(cors -> cors.configurationSource(...)) と明示的に渡しましたが、corsConfigurationSource という名前で CorsConfigurationSource Beanを定義しておけば、cors(Customizer.withDefaults()) と書くだけで自動的に拾ってくれます。
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults()) // corsConfigurationSource Beanを自動検出
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
return http.build();
}
Bean名が違うと検出されないので、メソッド名は corsConfigurationSource のままにしておくのが無難です。逆に、Beanは定義したのに cors() の呼び出し自体を書き忘れると「設定したはずなのにOPTIONSが401」という状態になります。Preflightで401が出たら、まず cors() があるかを確認してください。
exposedHeadersを忘れるとフロントからレスポンスヘッダーが読めない
CORSが通ったあとに意外とハマるのが、「レスポンスヘッダーがJavaScriptから取れない」問題です。
CORSの仕様では、ブラウザがJavaScriptに公開するレスポンスヘッダーは Content-Type や Cache-Control などごく一部に限られています。Authorization や Location、X-Total-Count のような独自ヘッダーを fetch の response.headers.get() で読むには、サーバー側で明示的に公開する必要があります。
config.setExposedHeaders(List.of("Authorization", "Location", "X-Total-Count"));
WebMvcConfigurerなら .exposedHeaders("Authorization", "Location") と書きます。JWTをレスポンスヘッダーで返す構成や、201 Created の Location をフロントで使いたい場合は、この設定がないと null しか返ってこないので気をつけましょう。
Preflightをcurlで確認する
設定が正しく適用されているかはcurlで確認できます。
curl -v -X OPTIONS http://localhost:8080/api/users \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type"
レスポンスに Access-Control-Allow-Origin: http://localhost:3000 が含まれていればOKです。含まれていない場合は設定が正しく適用されていません。Spring Securityを入れている場合、OPTIONSリクエストが401になっていないかもここで確認できます。
なお、よくあるミスとして allowCredentials(true) と allowedOrigins("*") の組み合わせ、SecurityFilterChainで cors() の記述を忘れてWebMvcConfigurerだけ設定している、などが挙げられます。設定が効いていない場合はまずこのcurlコマンドで動作確認してみてください。
正常系は通るのにエラーレスポンスにだけCORSヘッダーが付かない、というケースもあります。認証Filterなど、CORS処理より前のFilterで例外が投げられてそのままレスポンスが返っている、というのが典型です。コントローラー層の例外なら @RestControllerAdvice で捕まえて通常のレスポンスとして返しておけば、ブラウザにはCORSヘッダー付きのエラーレスポンスが届きます。本番向けの例外ハンドラの組み方は GlobalExceptionHandlerを本番運用向けに実装する で解説しています。
まとめ
4パターンの使い分けをまとめます。
| 状況 | 推奨パターン |
|---|---|
| Spring Securityなし・特定エンドポイントのみ | @CrossOrigin |
| Spring Securityなし・全体に一括適用 | WebMvcConfigurer |
| Spring Securityあり(本番環境) | SecurityFilterChain + CorsConfigurationSource |
| Spring MVC以外のサーブレットも同居 | CorsFilter Bean |
Spring Securityを使っているなら基本的にパターン3一択です。CorsConfigurationSource をBeanとして切り出しておくと設定の一元管理もしやすくなります。
認証・認可の実装については Spring Security JWTの記事 も合わせてどうぞ。FilterとInterceptorの仕組みをもっと詳しく知りたい方は InterceptorとFilterの違いと使い分け も参考になります。
参考リンク
本記事のコードはSpring Boot 3.x / Spring Security 6.xで動作確認しています。より詳しい仕様は以下の公式ドキュメントを参照してください。