Spring Boot の CORS 設定とは、別のオリジン(スキーム・ホスト・ポートの組)で動く画面からの呼び出しを、どのオリジン・メソッド・ヘッダーまで許可するかをサーバー側で宣言することです。 書き方は @CrossOrigin、WebMvcConfigurer#addCorsMappings、CorsConfigurationSource の3つがあり、Spring Security を使うかどうかで選び方が変わります。

この記事では、3方式のコード全文と比較表、Spring Security と併用するときの設定、application.yml から許可オリジンを読む書き方、つまずきやすい設定の「直す前と直した後」、curl でプリフライトを確かめる手順をまとめます。

確認環境:Spring Boot 4.1.1/Spring Framework 7.0.9/Spring Security 7.1.1 の公式リファレンスで確認(確認日 2026年10月7日)。Java 単体で再現できる部分は OpenJDK 21.0.10 と curl 8.5.0(Linux)で実行しています。

Spring Boot CORS設定の早見表

やりたいこと 書き方 置き場所
1つのAPIだけ許可 @CrossOrigin(origins = "...") コントローラー
アプリ全体をまとめて許可 addCorsMappings(CorsRegistry) WebMvcConfigurer
Spring Security と併用 UrlBasedCorsConfigurationSource の Bean + http.cors(withDefaults()) セキュリティ設定
フィルターチェーンごとに変える cors.configurationSource(...) 各 SecurityFilterChain
許可オリジンを環境ごとに変える @ConfigurationProperties + application.yml 設定クラス
Cookie を送る呼び出しを許可 allowCredentials(true) + オリジンを具体的に指定 どの方式でも
サブドメインをまとめて許可 allowedOriginPatterns("https://*.example.com") どの方式でも
プリフライトだけ別に処理 cors.preFlightRequestHandler(...) セキュリティ設定

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring Boot CORS設定|3つの書き方とSecurity併用のコード全文」

3つの書き方の比較表

方式 適用範囲 Security 併用時 向く場面
@CrossOrigin クラスまたはメソッド単位 http.cors(withDefaults()) を書くと MVC の設定が使われる 公開する API が少数で、API ごとに条件が違う
addCorsMappings URL パターン単位(アプリ全体) 同上 Security を使わない、または MVC 側に設定を寄せたい
CorsConfigurationSource URL パターン単位(フィルターで処理) Security がそのまま使う Security を使う API サーバー全般

Spring Security のリファレンスには、CORS は Spring Security より先に処理する必要があると書かれています。プリフライト(本番のリクエストの前にブラウザが送る OPTIONS)には Cookie などの認証情報が付かないため、認証が先に動くと、プリフライトが認証なしのリクエストとして断られるためです。Security を使うアプリでは、3つ目の CorsConfigurationSource を軸に考えると迷いにくくなります。

グローバル設定と @CrossOrigin を両方書いた場合、Spring Framework のリファレンスによるとオリジンやメソッドなどのリストは足し合わされ、allowCredentials と maxAge のように値が1つのものはローカル(@CrossOrigin)側が優先されます。

書き方1:@CrossOrigin でコントローラーに付ける

Spring Boot のリファレンスでは、@CrossOrigin を使うのに追加の設定は要らないとされています。

package com.example.demo.item;

import java.util.List;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@CrossOrigin(origins = "https://app.example.com", maxAge = 3600)
@RestController
@RequestMapping("/api/items")
public class ItemController {

    public record Item(long id, String name) { }

    // クラスに付けた設定がそのまま使われる
    @GetMapping
    public List<Item> list() {
        return List.of(new Item(1, "備品A"), new Item(2, "備品B"));
    }

    // メソッドに付けると、クラス側の設定に足し合わされる
    @CrossOrigin(origins = "https://admin.example.com")
    @DeleteMapping("/{id}")
    public void remove(@PathVariable long id) {
        // 削除処理
    }
}

@CrossOrigin を引数なしで付けたときの既定値は、Spring Framework のリファレンスで次のとおりです。

項目 引数なしのときの既定
オリジン すべて
ヘッダー すべて
メソッド そのメソッドにマッピングされた HTTP メソッド
allowCredentials 有効にならない
maxAge 30分

書き方2:WebMvcConfigurer.addCorsMappings で全体に設定する

package com.example.demo.config;

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", "https://admin.example.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowedHeaders("Authorization", "Content-Type")
            .exposedHeaders("Location")
            .allowCredentials(true)
            .maxAge(3600);
    }
}

registry.addMapping("/api/**") だけを書いた場合の既定値は、オリジンとヘッダーはすべて、メソッドは GET・HEAD・POST、maxAge は30分です。PUT や DELETE を使う API では、allowedMethods を明示します。

書き方3:CorsConfigurationSource と Spring Security を併用する

Spring Security のリファレンスでは、CorsConfigurationSource を渡して Security に組み込まれた CorsFilter を使うのが、CORS を先に処理させるいちばん簡単な方法とされています。自動で使われるのは UrlBasedCorsConfigurationSource 型の Bean がある場合なので、Bean メソッドの戻り値の型をこの型にしておきます。

package com.example.demo.config;

import static org.springframework.security.config.Customizer.withDefaults;

import java.util.List;

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;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // CorsConfigurationSource の Bean を使って、認証より前に CORS を処理する
            .cors(withDefaults())
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            );
        // 認証方式(フォームログイン、トークンなど)の設定は用途に合わせてここに足す
        return http.build();
    }

    @Bean
    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.setExposedHeaders(List.of("Location"));
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);

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

CorsConfigurationSource の Bean を用意せず、書き方1・2の MVC 側の設定だけがある場合も、http.cors(withDefaults()) を書けば Spring Security は Spring MVC に渡された CORS 設定を使います(Spring Security リファレンスの記載)。

なお、.cors(CorsConfigurer::disable) は「ブラウザの CORS の仕組みを止める」設定ではありません。リファレンスにあるとおり、Spring Security 側の CORS 対応を外すだけなので、別オリジンの画面からは呼び出せなくなります。

SecurityFilterChain ごとに CORS 設定を変える

CorsConfigurationSource の Bean が複数あると、Spring Security はどれを使うか決められないため自動設定を行いません。チェーンごとに cors.configurationSource(...) へ直接渡します。 この例では設定元を Bean にせず、普通のメソッドにしています。

package com.example.demo.config;

import java.util.List;

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.UrlBasedCorsConfigurationSource;

@Configuration
@EnableWebSecurity
public class MultiChainSecurityConfig {

    @Bean
    @Order(0)
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .cors((cors) -> cors
                .configurationSource(apiConfigurationSource())
            )
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            );
        return http.build();
    }

    @Bean
    @Order(1)
    public SecurityFilterChain siteFilterChain(HttpSecurity http) throws Exception {
        http
            .cors((cors) -> cors
                .configurationSource(siteConfigurationSource())
            )
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            );
        return http.build();
    }

    // /api/** 用:別オリジンの画面から、更新系も含めて許可
    UrlBasedCorsConfigurationSource apiConfigurationSource() {
        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"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    // それ以外:参照だけ許可
    UrlBasedCorsConfigurationSource siteConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("https://www.example.com"));
        configuration.setAllowedMethods(List.of("GET"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

許可オリジンを application.yml から読む

開発用と本番用で許可するオリジンが違うときは、値を application.yml に出し、@ConfigurationProperties で受け取ります。Spring Boot のリファレンスにあるとおり、引数付きコンストラクターが1つのクラス(レコードを含む)はコンストラクターで値が入り、@EnableConfigurationProperties か @ConfigurationPropertiesScan で有効にします。

# src/main/resources/application.yml
app:
  cors:
    allowed-origins:
      - "https://app.example.com"
      - "https://admin.example.com"
    allowed-methods:
      - GET
      - POST
      - PUT
      - DELETE
    allowed-headers:
      - Authorization
      - Content-Type
    exposed-headers:
      - Location
    allow-credentials: true
    max-age: 3600
# src/main/resources/application-local.yml(手元での開発用プロファイル)
app:
  cors:
    allowed-origins:
      - "http://localhost:5173"
package com.example.demo.config;

import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;

@ConfigurationProperties("app.cors")
public record AppCorsProperties(
        List<String> allowedOrigins,
        @DefaultValue({"GET", "POST"}) List<String> allowedMethods,
        @DefaultValue({"Authorization", "Content-Type"}) List<String> allowedHeaders,
        @DefaultValue List<String> exposedHeaders,
        @DefaultValue("false") boolean allowCredentials,
        @DefaultValue("1800") long maxAge) {
}
package com.example.demo.config;

import static org.springframework.security.config.Customizer.withDefaults;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
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;

@Configuration
@EnableWebSecurity
@EnableConfigurationProperties(AppCorsProperties.class)
public class PropertiesSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(withDefaults())
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            );
        return http.build();
    }

    @Bean
    UrlBasedCorsConfigurationSource corsConfigurationSource(AppCorsProperties props) {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(props.allowedOrigins());
        configuration.setAllowedMethods(props.allowedMethods());
        configuration.setAllowedHeaders(props.allowedHeaders());
        configuration.setExposedHeaders(props.exposedHeaders());
        configuration.setAllowCredentials(props.allowCredentials());
        configuration.setMaxAge(props.maxAge());

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

allowed-origins を書かないと allowedOrigins は null になり、CorsConfiguration の既定どおり「どのオリジンも許可しない」状態になります。設定の書き忘れが、そのまま広い許可につながらない形です。

CorsConfiguration の設定項目一覧

Spring Framework 7.0.9 の API 仕様書をもとにまとめました。new CorsConfiguration() の直後は、どのクロスオリジンのリクエストも許可しない状態です。

項目 役割 指定しないとき
allowedOrigins 許可するオリジン。具体的な値か "*" どのオリジンも許可しない
allowedOriginPatterns ホスト名に * を使える柔軟な指定 設定なし
allowedMethods 許可する HTTP メソッド。"*" で全メソッド GET と HEAD のみ
allowedHeaders プリフライトで許可するリクエストヘッダー 設定なし
exposedHeaders 画面側の JavaScript から読めるようにするレスポンスヘッダー 設定なし
allowCredentials Cookie などの資格情報つきの呼び出しを認めるか 認めない
allowPrivateNetwork プライベートネットワーク宛ての呼び出しを認めるか 認めない
maxAge プリフライトの結果をブラウザが覚えておく秒数 設定なし

applyPermitDefaultValues() を呼ぶと、未設定の項目に「全オリジン("*")、GET・HEAD・POST、全ヘッダー、maxAge 1800秒」が入ります。addCorsMappings や @CrossOrigin の既定値は、この値がもとになっています。

allowedOrigins と allowedOriginPatterns の違い

書き方 意味
https://app.example.com そのオリジンだけ
https://*.example.com example.com で終わるドメイン
https://*.example.com:[8080,8081] 上に加えて、ポート 8080 か 8081
https://*.example.com:[*] 上に加えて、既定のポートを含むすべてのポート

allowedOrigins で * を使えるのは「すべて」を表す "*" だけです。allowedOriginPatterns に一致した場合、Access-Control-Allow-Origin にはパターンや * ではなく、一致したオリジンそのものが入ります。 そのため allowCredentials(true) と組み合わせて使えます。

つまずきやすい設定:直す前と直した後

allowCredentials=true と "*" を一緒に書いた

CORS の仕様では、資格情報つきの応答で Access-Control-Allow-Origin: * を返せません。Spring Framework はこの組み合わせを受け付けず、CorsConfiguration#validateAllowCredentials が IllegalArgumentException を投げます。

// 直す前:資格情報を認めつつ、オリジンを "*" にしている
configuration.setAllowedOrigins(List.of("*"));
configuration.setAllowCredentials(true);
// 直した後(1):オリジンを具体的に並べる
configuration.setAllowedOrigins(List.of("https://app.example.com", "https://admin.example.com"));
configuration.setAllowCredentials(true);

// 直した後(2):サブドメインをまとめたいときはパターンで指定する
configuration.setAllowedOriginPatterns(List.of("https://*.example.com"));
configuration.setAllowCredentials(true);

allowedMethods と allowedHeaders の "*" は allowCredentials(true) と併用でき、その場合はプリフライトで要求されたメソッド・ヘッダーをそのまま返す形で処理されます。

Security より後ろで CORS を処理してプリフライトが 401 になる

MVC 側(書き方1・2)にだけ CORS 設定があり、セキュリティ設定に cors の記述が無いと、プリフライトが認証の段階で断られます。

// 直す前:cors の指定が無い
http
    .authorizeHttpRequests((authorize) -> authorize
        .anyRequest().authenticated()
    );
// 直した後:cors を有効にして、認証より前に処理させる
http
    .cors(withDefaults())
    .authorizeHttpRequests((authorize) -> authorize
        .anyRequest().authenticated()
    );

この順序の違いを、Java 21 の標準ライブラリだけで作った小さなサーバーで再現しました。ポート 10390 は「認証 → CORS」、10391 は「CORS → 認証」の順で処理します(Spring そのものではなく、処理順の影響だけを取り出したものです)。

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Set;

/** 認証とCORSの処理順でプリフライトの結果が変わることを、JDKだけで再現する。 */
public class CorsOrderDemo {

    static final Set<String> ALLOWED_ORIGINS = Set.of("https://app.example.com");

    public static void main(String[] args) throws IOException {
        start(10390, false); // 認証 → CORS の順
        start(10391, true);  // CORS → 認証 の順
    }

    static void start(int port, boolean corsFirst) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 0);
        server.createContext("/api/items", ex -> {
            if (corsFirst) {
                if (handleCors(ex)) return;
                if (rejectUnlessAuthenticated(ex)) return;
            } else {
                if (rejectUnlessAuthenticated(ex)) return;
                if (handleCors(ex)) return;
            }
            send(ex, 200, "[{\"id\":1,\"name\":\"備品A\"}]");
        });
        server.start();
    }

    /** Authorization ヘッダーが無ければ 401 を返して true。 */
    static boolean rejectUnlessAuthenticated(HttpExchange ex) throws IOException {
        if (ex.getRequestHeaders().getFirst("Authorization") == null) {
            send(ex, 401, "Unauthorized");
            return true;
        }
        return false;
    }

    /** CORS ヘッダーを付ける。プリフライトならここで応答を終えて true。 */
    static boolean handleCors(HttpExchange ex) throws IOException {
        String origin = ex.getRequestHeaders().getFirst("Origin");
        if (origin == null) return false;
        boolean preflight = ex.getRequestMethod().equals("OPTIONS")
                && ex.getRequestHeaders().getFirst("Access-Control-Request-Method") != null;
        if (!ALLOWED_ORIGINS.contains(origin)) {
            if (preflight) { send(ex, 403, "Invalid CORS request"); return true; }
            return false;
        }
        var h = ex.getResponseHeaders();
        h.add("Vary", "Origin");
        h.set("Access-Control-Allow-Origin", origin);
        h.set("Access-Control-Allow-Credentials", "true");
        if (preflight) {
            h.set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE");
            h.set("Access-Control-Allow-Headers", "Authorization, Content-Type");
            h.set("Access-Control-Max-Age", "3600");
            send(ex, 200, "");
            return true;
        }
        return false;
    }

    static void send(HttpExchange ex, int status, String body) throws IOException {
        byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
        ex.sendResponseHeaders(status, bytes.length == 0 ? -1 : bytes.length);
        if (bytes.length > 0) ex.getResponseBody().write(bytes);
        ex.close();
    }
}

java CorsOrderDemo.java で起動し、同じプリフライトを両方のポートに送った結果です。

$ curl -i -X OPTIONS http://127.0.0.1:10390/api/items -H 'Origin: https://app.example.com' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: authorization,content-type'
HTTP/1.1 401 Unauthorized
Date: Wed, 07 Oct 2026 13:27:48 GMT
Content-length: 12

Unauthorized
$ curl -i -X OPTIONS http://127.0.0.1:10391/api/items -H 'Origin: https://app.example.com' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: authorization,content-type'
HTTP/1.1 200 OK
Access-control-allow-headers: Authorization, Content-Type
Access-control-max-age: 3600
Date: Wed, 07 Oct 2026 13:27:49 GMT
Access-control-allow-methods: GET,POST,PUT,DELETE
Access-control-allow-origin: https://app.example.com
Vary: Origin
Access-control-allow-credentials: true
Content-length: 0

認証が先だと、プリフライトには Authorization も Cookie も付かないので 401 になり、CORS のヘッダーが1つも返りません。 ブラウザはこの時点で本番のリクエストを送らないため、画面側には CORS のエラーとして見えます。CORS を先に処理すると 200 と許可ヘッダーが返り、続く本番のリクエストで認証が行われます。

自前の CorsFilter を Bean にして Security と二重に持った

Spring Framework のリファレンスには、CorsFilter を Spring Security と一緒に使う場合は、Security に組み込みの CORS 対応があることに留意するよう注記があります。

// 直す前:CorsFilter を自分で Bean にしている(Security のフィルターとの前後関係を自分で管理する形)
@Bean
CorsFilter corsFilter() {
    return new CorsFilter(corsConfigurationSource());
}
// 直した後:設定元(UrlBasedCorsConfigurationSource)だけを Bean にして、組み込みは Security に任せる
@Bean
UrlBasedCorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(List.of("https://app.example.com"));
    configuration.setAllowedMethods(List.of("GET", "POST"));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}
// SecurityFilterChain 側には .cors(withDefaults()) を書く

PUT・DELETE だけ通らない

addMapping の既定は GET・HEAD・POST、CorsConfiguration で allowedMethods を指定しないときは GET と HEAD だけです。

// 直す前:メソッドを指定していない
registry.addMapping("/api/**").allowedOrigins("https://app.example.com");
// 直した後:使うメソッドを並べる
registry.addMapping("/api/**")
    .allowedOrigins("https://app.example.com")
    .allowedMethods("GET", "POST", "PUT", "DELETE");

レスポンスヘッダーを画面側で読めない

Location や独自ヘッダーを JavaScript から読むには exposedHeaders に名前を並べます。Cache-Control・Content-Language・Expires・Last-Modified・Pragma は、並べなくても読めるヘッダーです。

// 直した後
configuration.setExposedHeaders(List.of("Location", "X-Total-Count"));

curl でプリフライトを確認する

ブラウザを開かなくても、プリフライトは curl で再現できます。Origin と Access-Control-Request-Method の2つを付けた OPTIONS がプリフライトです。

# プリフライト(本番で POST と Authorization・Content-Type ヘッダーを使う想定)
curl -i -X OPTIONS http://localhost:8080/api/items \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: authorization,content-type'

# 本番のリクエスト(Origin を付けて、応答の許可ヘッダーを見る)
curl -i http://localhost:8080/api/items \
  -H 'Origin: https://app.example.com' \
  -H 'Authorization: Bearer <トークン>'

期待する応答ヘッダーの読み方は次のとおりです。

応答ヘッダー 見るところ
ステータス プリフライトが 200 番台であること。401 なら処理順、403 なら許可条件を見直す
Access-Control-Allow-Origin 送った Origin と同じ値か(資格情報なしなら * も可)
Access-Control-Allow-Methods 本番で使うメソッドが含まれるか
Access-Control-Allow-Headers 本番で送るヘッダーが含まれるか
Access-Control-Allow-Credentials Cookie を送る場合に true か
Access-Control-Max-Age プリフライトの結果を覚えておく秒数
Vary: Origin オリジンごとに応答が変わることをキャッシュに伝えているか

先ほどの Java 21 のサーバーに、許可していないオリジンからプリフライトを送ると次のようになります。許可ヘッダーが付かず 403 で終わる形で、Spring Framework の PreFlightRequestHandler の仕様(CORS ヘッダーを付けるか、HttpStatus.FORBIDDEN で断る)と同じ考え方です。

$ curl -i -X OPTIONS http://127.0.0.1:10391/api/items -H 'Origin: https://other.example.net' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: authorization,content-type'
HTTP/1.1 403 Forbidden
Date: Wed, 07 Oct 2026 13:27:49 GMT
Content-length: 20

Invalid CORS request

許可したオリジンから認証つきで本番のリクエストを送った結果です。本番の応答には Allow-Methods などは付かず、Allow-Origin と Allow-Credentials が付きます。

$ curl -i http://127.0.0.1:10391/api/items -H 'Origin: https://app.example.com' -H 'Authorization: Bearer x'
HTTP/1.1 200 OK
Date: Wed, 07 Oct 2026 13:27:49 GMT
Access-control-allow-origin: https://app.example.com
Vary: Origin
Access-control-allow-credentials: true
Content-length: 27

[{"id":1,"name":"備品A"}]

ヘッダー名の大文字・小文字は HTTP では区別されません。サーバーによって Access-control-allow-origin のように表示されても同じ意味です。

PreFlightRequestHandler という新しい選択肢

PreFlightRequestHandler は、プリフライト(OPTIONS)の処理だけを担当するインターフェースです。Spring Framework の API 仕様書では 6.2 からの提供で、メソッドは handlePreFlight(HttpServletRequest, HttpServletResponse) の1つ、実装クラスとして HandlerMappingIntrospector が挙げられています。

Spring Security 7.1.1 のリファレンスによると、チェーンに PreFlightRequestHandler が選ばれた場合、Security は PreFlightRequestFilter を CorsFilter より前に登録し、プリフライトを早い段階で処理します。渡し方は2つあります。

渡し方 条件
cors.preFlightRequestHandler(...) で直接渡す そのチェーンで明示的に指定
PreFlightRequestHandler の Bean を登録する cors が有効で、そのチェーンに CorsConfigurationSource も CorsFilter も選ばれていないとき
package com.example.demo.config;

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.PreFlightRequestHandler;

@Configuration
@EnableWebSecurity
public class PreFlightSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http,
            PreFlightRequestHandler preFlightRequestHandler) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            )
            .cors((cors) -> cors
                .preFlightRequestHandler(preFlightRequestHandler)
            );
        return http.build();
    }
}

同じ cors の設定の中で configurationSource と preFlightRequestHandler の両方を指定すると、起動時にエラーになります。 どちらか一方を選びます。@CrossOrigin や addCorsMappings など MVC 側の設定を正としてプリフライトを処理させたいときに候補になる方法です。

対象バージョンとサポート期間

Spring Boot の世代ごとの OSS サポート期間です(spring.io の公開 API、確認日 2026年10月7日)。

世代 初回リリース OSS サポート終了
3.5.x 2025-05-31 2026-06-30
4.0.x 2025-11-30 2026-12-31
4.1.x 2026-06-30 2027-07-31

Spring Boot 4.0 の移行ガイドでは、Web 用スターターの名前が spring-boot-starter-web から spring-boot-starter-webmvc に変わっています。この記事のコードは Spring MVC(サーブレット)向けです。

CORS のエラーメッセージ別の原因と、フロント側・サーバー側のどちらで直すかの切り分けは、関連記事「CORSエラーの原因と解決方法|エラーメッセージ別に解説」で扱っています。この記事は Spring Boot 側の書き方に絞っています。

よくある質問

Q. 3つの書き方のうち、どれを選べばよいですか? A. Spring Security を使うなら UrlBasedCorsConfigurationSource の Bean と http.cors(withDefaults()) の組み合わせが分かりやすい選択です。Security を使わないアプリなら addCorsMappings、API ごとに条件が違う少数の公開なら @CrossOrigin が向いています。

Q. @CrossOrigin を付けたのに、Security を入れたらプリフライトが通らなくなりました。 A. セキュリティ設定に .cors(withDefaults()) を足します。CorsConfigurationSource の Bean が無い場合、Spring Security は Spring MVC に渡された CORS 設定(@CrossOrigin や addCorsMappings)を使います。

Q. Cookie を使うログインで、許可オリジンを増やしたいときは? A. allowCredentials(true) のときは "*" を使えないので、allowedOrigins に具体的なオリジンを並べるか、allowedOriginPatterns("https://*.example.com") のようにパターンで指定します。

Q. maxAge はどのくらいにすればよいですか? A. addCorsMappings と @CrossOrigin の既定は30分(1800秒)です。設定を頻繁に変える開発中は短め、落ち着いたら 3600 秒などに伸ばすと、プリフライトの回数を抑えられます。

まとめ

  • 書き方は3つ:@CrossOrigin(API 単位)、addCorsMappings(アプリ全体)、CorsConfigurationSource(フィルターで処理)
  • Spring Security と併用するなら、CORS を認証より先に処理させる。UrlBasedCorsConfigurationSource の Bean と http.cors(withDefaults()) を組み合わせる
  • CorsConfigurationSource が複数あるときは自動設定されないので、チェーンごとに cors.configurationSource(...) で渡す
  • allowCredentials(true) と "*" は併用できない。具体的なオリジンか allowedOriginPatterns を使う
  • メソッドの既定は方式で違う:addMapping は GET・HEAD・POST、CorsConfiguration の未指定は GET・HEAD
  • プリフライトは curl -X OPTIONS に Origin と Access-Control-Request-Method を付けて確認できる
  • PreFlightRequestHandler は configurationSource と同時に指定しない

参考・出典

確認日はいずれも 2026年10月7日です。

  • Spring Security 7.1.1 リファレンス「CORS」 https://docs.spring.io/spring-security/reference/servlet/integrations/cors.html
  • Spring Framework 7.0.9 リファレンス「CORS」 https://docs.spring.io/spring-framework/reference/web/webmvc-cors.html
  • Spring Framework 7.0.9 API「CorsConfiguration」 https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/cors/CorsConfiguration.html
  • Spring Framework 7.0.9 API「PreFlightRequestHandler」 https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/cors/PreFlightRequestHandler.html
  • Spring Boot 4.1.1 リファレンス「Servlet Web Applications(CORS Support)」 https://docs.spring.io/spring-boot/reference/web/servlet.html
  • Spring Boot 4.1.1 リファレンス「Externalized Configuration」 https://docs.spring.io/spring-boot/reference/features/external-config.html
  • Spring Boot 4.0 Migration Guide https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide
  • Spring Boot 世代別サポート期間 https://api.spring.io/projects/spring-boot/generations

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring Boot CORS設定|3つの書き方とSecurity併用のコード全文」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?