スポンサーリンク

Spring BootのCORSエラー解決!最小設定で安全に動かす

スポンサーリンク
この記事は約18分で読めます。
スポンサーリンク

ブラウザからSpring BootのAPIを叩いた瞬間に、画面がこう言って止まるやつありますよね。

「CORS policyでブロックされました」みたいなやつです。いや、こっちはただ fetch() しただけなんですけど…ってなりますよね。

典型パターンはこれです。

フロントが React/Vue(http://localhost:3000、バックが Spring Boot(http://localhost:8080
Postmanだと普通に200返ってくるのに、ブラウザだけ失敗する。
これ、サーバーが壊れてるというより、ブラウザ側の安全ルールに引っかかってる可能性が高いです。

この記事では、まずCORSの正体を1分でつかんでから、最小の設定だけで動かすコピペを2パターン用意します。

  • Spring Securityなし
  • Spring Securityあり

さらに、よくある地雷(OPTIONSが通ってない、*の危険、Cookieあり/なし問題)も先に潰します。

最短ゴールは「localhost:3000 → 8080 がブラウザでも通る」状態にすることです。
危ない設定でごまかさず、安全にスッと直していきましょう。

  1. CORSは「ブラウザの安全ルール」:まず1分で理解
    1. なぜブラウザだけ失敗?(Postmanが通る理由)
    2. 例でイメージ:localhost:3000 → localhost:8080
    3. CORSで決まる3つ(Origin / Method / Header)
  2. まずこれだけ!最小の正解(コピペ2パターン)
    1. パターンA:Spring Securityなし → WebMvcConfigurerでOK
    2. パターンB:Spring Securityあり → Security側にもCORSが必要
    3. 許可は必要最小限:Origin / Method / Header の決め方
    4. 「Cookieを使う/使わない」で設定が変わる(超重要)
  3. よくある詰まりポイントTOP3(先に地雷を避ける)
    1. preflight(OPTIONS)が通ってない問題(いちばん多い)
    2. allowedOrigins と allowedOriginPatterns の違いで詰む
    3. allowCredentials=true なのに Origin=* にしてる(危ない&動かない)
  4. 開発用と本番用を分けよう(安全な許可の考え方)
    1. 開発:localhostだけ許可でOK(例:3000だけ)
    2. 本番:自分のドメインだけ許可(*は基本やめる)
    3. やっていい/ダメ早見表
    4. 設定を切り替える方法(profiles / 環境変数の方針)
  5. まだ直らないときの診断フロー(順番に潰せばOK)
    1. エラー文の読み方(どこがダメか当てるコツ)
    2. ブラウザで見る場所:NetworkでOPTIONS/Response Header確認
    3. ありがちな見落とし:URL末尾、http/https、ポート、リダイレクト
  6. 最後のチェックリスト&まとめ(うっかり危険設定を防ぐ)
    1. 10秒チェックリスト
    2. 「全部許可していいのはいつ?」限定条件だけ
    3. 次にやること(チーム共有テンプレ/本番前の確認)
  7. よくある質問
スポンサーリンク

CORSは「ブラウザの安全ルール」:まず1分で理解

CORSって聞くと急にむずかしそうですが、正体はめちゃシンプルです。

「ブラウザは、知らない場所に勝手に情報を渡さないように“門番”を置いてる」ってだけです。

なので、サーバー側が「この人は入っていいですよ〜」って許可の札(ヘッダー)を出してあげないと、ブラウザが止めちゃいます。

なぜブラウザだけ失敗?(Postmanが通る理由)

ポイントはこれです:CORSは“ブラウザだけ”が守る安全ルールです。

Postmanやcurlは「テスト用の道具」なので、門番チェックをしません。だから Postmanは通るのに、ブラウザだけ落ちるが起きます。

つまり、API自体が死んでるとは限らず、ブラウザが「許可札ないよ?」って言ってるだけのケースが多いです。

例でイメージ:localhost:3000 → localhost:8080

たとえばこの形、超あるあるです。

  • フロント:http://localhost:3000(React/Vue)
  • API:http://localhost:8080(Spring Boot)

ブラウザから見ると、ポートが違うだけでも“別の場所(別オリジン)”扱いです。
なので 3000 → 8080 の時点で、ブラウザは「ほんとに行っていい?」って確認モードに入ります。

CORSで決まる3つ(Origin / Method / Header)

CORSでチェックされやすいのは主にこの3つです。

  • Origin:どこ(どのURL)から来たリクエスト?(例:http://localhost:3000
  • Method:やることは何?(GET/POST/PUT…)
  • Header:追加の情報は何を送る?(例:Content-TypeAuthorization など)

サーバーがレスポンスで、たとえば Access-Control-Allow-Origin: http://localhost:3000 みたいに許可の札を返すと、ブラウザは「OK、通ってよし!」になります。

逆にこの札がない・条件がズレてると、ブラウザは容赦なくブロックします。

まずこれだけ!最小の正解(コピペ2パターン)

最初に分岐です。あなたはどっちですか?

  • Spring Security入れてないspring-boot-starter-security なし / 認証まわり触ってない)→ パターンA
  • Spring Security入れてる(依存関係にある / 401・403が絡む)→ パターンB

パターンA:Spring Securityなし → WebMvcConfigurerでOK

// CorsConfig.java
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 CorsConfig implements WebMvcConfigurer {
  @Override
  public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
      // ★ここだけ自分のフロントURLに変える
      .allowedOrigins("http://localhost:3000")
      // ★必要なものだけ(例)
      .allowedMethods("GET", "POST")
      // ★必要なものだけ(例:JSON + 認証ヘッダー)
      .allowedHeaders("Content-Type", "Authorization")
      // Cookie使うなら true(後で説明)
      .allowCredentials(false);
  }
}

パターンB:Spring Securityあり → Security側にもCORSが必要

Securityありは「MVCで許可しても、Securityフィルターで止まる」ことがあるので、SecurityにCORSを渡すのがコツです。

// SecurityConfig.java(Spring Boot 3 / Security 6系の書き方)
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
public class SecurityConfig {

  @Bean
  SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
      .cors(Customizer.withDefaults()) // ★CORS有効化(重要)
      .csrf(csrf -> csrf.disable())     // 開発中の例(必要に応じて)
      .authorizeHttpRequests(auth -> auth
        .requestMatchers("/**").permitAll() // 例:まず動かす(本番は要調整)
      )
      .build();
  }

  @Bean
  CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    // ★ここだけ自分のフロントURLに変える
    config.setAllowedOrigins(List.of("http://localhost:3000"));
    config.setAllowedMethods(List.of("GET", "POST", "OPTIONS")); // OPTIONSも忘れがち
    config.setAllowedHeaders(List.of("Content-Type", "Authorization"));
    config.setAllowCredentials(false); // Cookie使うなら true(後で説明)

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

許可は必要最小限:Origin / Method / Header の決め方

  • Origin:フロントのURLを“丸ごと一致”で入れる(http/https、ポートまで)
  • Method:実際に使うものだけ(まずは GET/POST で十分なこと多いです)
  • Header:だいたい最初はこれだけでOK
    • JSON送る → Content-Type
    • Bearerトークン送る → Authorization

※そして実務で一番役立つのは、ブラウザのDevTools(Networkタブ)です。CORSはブラウザが止めるので、ここを見るのが最短ルートです。

「Cookieを使う/使わない」で設定が変わる(超重要)

Cookie(セッション/HttpOnly Cookieなど)を使うなら:

  • allowCredentials(true) が必要
  • その代わり Originを * にできません(仕様的にNG&危ない)
  • フロントも fetch / axios で credentials付きにする必要あり
// fetch例(Cookie使うとき)
fetch("http://localhost:8080/api", {
  method: "GET",
  credentials: "include",
});

Cookieを使わない(AuthorizationヘッダーでBearerなど)なら:

  • 基本は allowCredentials(false) のままでOK(安全寄り)
  • ただし Authorization を付けると preflight(OPTIONS)が飛びやすいので、上の設定みたいに OPTIONSも通すのが安定です。
筆者
筆者

この章のゴールはここです:「まず最小で動く形」を固定して、あとで必要な分だけ狭める

次の章で、初心者が踏みがちな地雷(特にOPTIONS)を先に避けにいきます。

よくある詰まりポイントTOP3(先に地雷を避ける)

CORSって「設定したはずなのに動かない…」が起きやすいんですが、だいたいハマり所はこの3つに集約されます。症状から当てにいきましょう。

preflight(OPTIONS)が通ってない問題(いちばん多い)

  • 症状:ブラウザのConsoleに「preflightが通ってない」系の文言が出る/Networkを見ると OPTIONS が 403/404 になってる
  • 原因:ブラウザが本番リクエストの前にする“確認電話”=OPTIONS を、サーバー(やSecurity)が弾いてる
  • 直し方(1行)OPTIONSを通す + CORS設定を返す(SecurityありならSecurity側で .cors() も必須)

イメージはこんな流れです:

① OPTIONS /api(確認) → 200 + Allow系ヘッダーOK
② GET/POST /api(本番) → 200

ここで①が落ちたら、②は永遠に来ません。まずOPTIONSを見てください。

allowedOrigins と allowedOriginPatterns の違いで詰む

  • 症状:「許可したはずのOriginなのにダメ」になる/ワイルドカードっぽく書いたのに一致しない
  • 原因allowedOrigins完全一致向きallowedOriginPatternsワイルドカードOK向き、という性格の違いでズレる
  • 直し方(1行):開発中はまず allowedOrigins("http://localhost:3000") みたいに固定、サブドメイン等でパターンが必要なら allowedOriginPatterns を使う

※「とりあえずワイルドカードで…」は事故りやすいので、最初は固定が正解です。

allowCredentials=true なのに Origin=* にしてる(危ない&動かない)

  • 症状credentials: "include"(Cookieあり)にした瞬間、ブラウザが「* じゃダメ」って怒る
  • 原因Cookieを許可する設定(allowCredentials=true)と、Origin=*(全許可)はセットにできません(安全的にも仕様的にもNG)
  • 直し方(1行):Cookieを使うなら Originは必ず具体的に指定(例:http://localhost:3000)+フロント側も credentials: "include" を付ける

⚠️ここ、動かないだけじゃなくて危ないので「*で雑に直す」はやめた方がいいです。

※切り分けに使う“道具”は無料で十分です:ブラウザのNetworkログと、Spring Bootのコンソールログ。どこで止まってるかが見えるだけで、解決スピードが一気に上がります。

開発用と本番用を分けよう(安全な許可の考え方)

CORSで一番やりがち事故が、「動かないから * で全許可!」→そのまま本番に出しちゃうやつです。これ、あとで普通に燃えます。なので合言葉いきますね。

合言葉:開発は小さく許可。本番はもっと小さく許可。

許可が広いほどラクですが、広いほど「変なところから叩けちゃう入口」も増えます。

開発:localhostだけ許可でOK(例:3000だけ)

開発中は相手が自分のPCなので、http://localhost:3000 だけ許可でだいたい十分です。
「とりあえず動かす」も大事ですが、“必要な相手だけ通すクセ”をここで作るのが大事です。

本番:自分のドメインだけ許可(*は基本やめる)

本番は、たとえばフロントが https://app.example.com なら、それだけ許可が基本です。

* は「世界中のサイトから呼べます」になりやすいので、特殊な理由がない限り避けた方が安全です。

やっていい/ダメ早見表

環境やっていい例ダメ寄り例
開発http://localhost:3000 のみ許可* を常用
本番https://app.example.com のみ許可*/不要な複数ドメイン許可

設定を切り替える方法(profiles / 環境変数の方針)

細かい実装より方針だけ押さえるとOKです。

  • dev/prodで「許可Originの値だけ」分ける(コードは同じでもOK)
  • 代表例:application-dev.ymlapplication-prod.yml に分ける、または 環境変数で CORS_ALLOWED_ORIGINS を差し替える

※本番で「自分のドメインだけ許可」を自然にやるには、前提として 独自ドメイン(年数千円〜) と、フロント配信(例:Vercel/Netlifyの無料枠〜有料)みたいな土台があるとスムーズです。

筆者
筆者

次の章では、まだハマりやすい地雷(特にOPTIONSまわり)を診断フローに落として、確実に潰していきます。

まだ直らないときの診断フロー(順番に潰せばOK)

「設定したのに無理!」ってときは、寄り道せず この順番で見れば当たりやすいです。

エラー文の読み方(どこがダメか当てるコツ)

  • No 'Access-Control-Allow-Origin' header → 返事に“許可札”が出てない(設定漏れ or その経路を通ってない)
  • Response to preflight request doesn't pass → まず OPTIONS(確認電話) が落ちてます
  • CORS policy: ... redirected → リダイレクトが混ざってCORSが崩れてるかも

ブラウザで見る場所:NetworkでOPTIONS/Response Header確認

  • DevTools → Network を開く
  • まず OPTIONS があるか探す(あったらそこが優先)
  • Statusが200系かを見る(403/404ならそこが原因)
  • Response Headersでこれを“探し物”してください
    • Access-Control-Allow-Origin
    • Access-Control-Allow-Methods
    • Access-Control-Allow-Headers
    • Cookie使うなら Access-Control-Allow-Credentials: true

ありがちな見落とし:URL末尾、http/https、ポート、リダイレクト

  • フロントのOriginが想定通り?(http://localhost:3000 になってる?)
  • httphttps 混ざってない?(これ別物です)
  • ポート違い(3000/5173/8080など)を許可できてる?
  • URL末尾の / の有無で別ルート扱いになってない?
  • APIが 別URLへリダイレクトしてない?(ログイン画面へ飛ぶ等)

最後のチェックリスト&まとめ(うっかり危険設定を防ぐ)

ここまでで「とりあえず動く」は作れたはずなので、最後に 事故らないための10秒チェックだけ置いておきます。これ、チームのテンプレにもそのまま使えます。

10秒チェックリスト

  • 許可Originは 必要なものだけ(開発なら http://localhost:3000 だけ)
  • * を使ってない(使うなら理由が言える)
  • 使うMethodだけ許可してる(例:GET/POST
  • 使うHeaderだけ許可してる(例:Content-Type / Authorization
  • OPTIONS(preflight)が200で返ってる
  • Securityありなら Security側で .cors() が有効
  • Cookie使うなら allowCredentials(true)Originは具体指定*禁止)
  • フロントの http/https・ポートが一致してる(3000/5173問題)
  • リダイレクトしてない(ログイン画面に飛んでCORS崩れがち)
  • 本番は 自ドメインだけ許可になってる

「全部許可していいのはいつ?」限定条件だけ

正直、基本はおすすめしません。それでも「全部許可(広め許可)」が許されるのは、せめてこの3条件が揃うときだけにしてください。

  • 期間:短時間(今日だけ、検証の1時間だけ、など)
  • 範囲:限られた用途(特定の検証・デバッグだけ)
  • 場所:閉じた環境(社内ネットワーク、ローカル環境、VPN内など)

この3つのどれかが欠けるなら、* はやめた方が安全です。

次にやること(チーム共有テンプレ/本番前の確認)

次の一手はこれが強いです。

  • CORS設定テンプレ(開発用/本番用)を作ってチームで共有する
  • 本番前レビューのチェック項目に、さっきの 10秒チェックリストをそのまま入れる

共有先は、カテゴリとしては「チーム共有ツール」でOKで、価格帯も 無料〜で十分です(例:Notion / Google Docs / GitHub Gist)。
一回テンプレ化すると、CORS事故はかなり減りますよ。

よくある質問

Q
Postmanだと動くのに、ブラウザだけCORSで止まるのはなぜですか?
A

CORSはブラウザだけが守る安全ルールだからです。Postmanはそのチェックをしないので通ります。つまり「APIが壊れてる」じゃなくて「ブラウザが門番してる」ことが多いです。

Q
localhostなのにCORSになるのはおかしくないですか?
A

おかしくないです。localhostでも ポートが違うだけで別物扱いです。
例:http://localhost:3000http://localhost:8080 は別オリジンです。

Q
とりあえず Access-Control-Allow-Origin: * にしたらダメですか?
A

動くことはありますが、基本おすすめしません。
特に Cookieを使う(credentialsあり)場合は * は仕様的にNGで、危ない上に動きません。

Q
preflight(OPTIONS)って何ですか?なんで勝手に飛ぶんですか?
A

ブラウザの「本番前の確認電話」みたいなものです。
Authorization ヘッダーを付けたり、Content-Type: application/json だったりすると、ブラウザが先にOPTIONSで「この条件で送っていい?」を確認します。

Q
OPTIONSが403/404になります。何を直せばいいですか?
A

まずはここを疑ってください。

  • Spring SecurityがOPTIONSを弾いている
  • CORS設定がSecurity側に届いていない(.cors()してない等)
  • そもそもOPTIONSを許可する設定がない

ブラウザのNetworkで OPTIONSのStatus を見て、そこから潰すのが早いです。

Q
Spring Securityを使ってると、CORS設定はどこに書けばいいですか?
A

Security側にもCORSが必要になることが多いです。
「MVCで許可したのに動かない」場合、Securityフィルターで止まってる可能性が高いです。

Q
allowedOriginsallowedOriginPatterns はどう使い分けますか?
A

ざっくりこれです。

  • allowedOrigins完全一致で許可したいとき(最初はこれが安全)
  • allowedOriginPatterns:サブドメインなど、パターンで許可したいとき(ワイルドカード寄り)

開発中はまず http://localhost:3000 を完全一致で許可するのが安定です。

Q
Cookie(セッション)を使う場合、何が変わりますか?
A

重要ポイントが増えます。

  • サーバー:allowCredentials(true) が必要
  • ただし Originを * にできません(具体指定が必要)
  • フロント:fetch/axiosで credentials付きにする必要があります

ここがズレると「設定したのにダメ」が起きがちです。

Q
開発と本番でCORS設定は分けるべきですか?
A

分けた方が安全です。

  • 開発:http://localhost:3000 だけ許可で十分
  • 本番:https://app.example.com みたいに 自分のドメインだけ許可が基本

「開発で広げた設定を本番に持ち込む」のが事故パターンです。

Q
どこを見れば原因が一番早く分かりますか?
A

ブラウザの DevTools → Network が最短です。

  • OPTIONSが飛んでるか
  • それが200か(403/404ならそこが原因)
  • Response Headersに Access-Control-Allow-Origin などが付いてるか

これだけで「どこで止まってるか」がかなり当てられます。

タイトルとURLをコピーしました