Spring BootでCORSを設定する方法 - @CrossOriginとWebMvcConfigurerの使い分け


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:3000http://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を使った設定切り替え が参考になります。

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コマンドで動作確認してみてください。

まとめ

3パターンの使い分けをまとめます。

状況推奨パターン
Spring Securityなし・特定エンドポイントのみ@CrossOrigin
Spring Securityなし・全体に一括適用WebMvcConfigurer
Spring Securityあり(本番環境)SecurityFilterChain + CorsConfigurationSource

Spring Securityを使っているなら基本的にパターン3一択です。CorsConfigurationSource をBeanとして切り出しておくと設定の一元管理もしやすくなります。

認証・認可の実装については Spring Security JWTの記事 も合わせてどうぞ。FilterとInterceptorの仕組みをもっと詳しく知りたい方は InterceptorとFilterの違いと使い分け も参考になります。