SSRとSPAでCORS設計が変わる理由をブラウザセキュリティモデルから解説
CORS(Cross-Origin Resource Sharing)は、SPAを作ると突然意識するようになる仕組みです。一方でSSR中心の構成では、ほとんど意識せずに済む場合があります。
これは設定の難易度の違いではありません。通信の主体がブラウザかサーバかという、セキュリティモデルの違いによるものです。
CORSはサーバの機能ではなく、ブラウザの防御機構です。ここを理解すると「なぜSPAだけCORSエラーが出るのか」がはっきりします。
同一オリジンポリシーとは何か
ブラウザは基本的に、異なるオリジン間の通信を制限します。これを同一オリジンポリシーと呼びます。
オリジンは次の3つの組み合わせです。
- スキーム(http / https)
- ホスト名
- ポート
たとえば次は別オリジンです。
JavaScriptから別オリジンへアクセスしようとすると、ブラウザがブロックします。これを緩和する仕組みがCORSです。
SPAでCORSが必要になる理由
SPAでは、ブラウザが直接APIを呼びます。
fetch("https://api.example.com/users");
このときブラウザは「別サイトへのアクセス」と判断し、サーバに許可を要求します。これがプリフライトリクエストです。
OPTIONS /users Origin: https://example.com
サーバが許可すると、実際の通信が行われます。
Access-Control-Allow-Origin: https://example.com
このヘッダがないと、レスポンスは届いていてもJavaScriptから参照できません。ここがCORSエラーの正体です。
SSRではなぜCORSを意識しないのか
SSRではブラウザがAPIを呼びません。サーバが呼びます。
- ブラウザ → SSRサーバ
- SSRサーバ → APIサーバ
サーバ間通信には同一オリジンポリシーが適用されません。ブラウザのセキュリティ機構だからです。
つまりCORSは発生しません。単なるHTTP通信です。
ここが大きな違いです。SPAはブラウザが通信主体、SSRはサーバが通信主体です。
CORSと認証の関係
さらに混乱を招くのが認証です。Cookieを使う場合、次の設定が必要になります。
Access-Control-Allow-Credentials: true Access-Control-Allow-Origin: https://example.com
ワイルドカード(*)は使えません。ブラウザが認証情報付き通信を拒否するためです。
この制約により、SPAでは環境ごとにCORS設定が必要になります。
- localhost
- staging
- production
設定漏れがログイン失敗として現れます。
プリフライトが性能に影響する
CORSにはOPTIONSリクエストが発生します。これがプリフライトです。
特に次の場合に起きます。
- カスタムヘッダ
- JSON POST
- 認証ヘッダ
つまり、一般的なSPAのAPI通信はほぼ毎回プリフライトが発生します。通信回数が2倍になり、レイテンシが増加します。
OPTIONS /api → 200 POST /api → 200
モバイル回線では体感速度に影響が出ます。
SSRとSPAの設計判断
この違いにより、API公開範囲の考え方が変わります。
SPA:
- 公開APIとして扱う必要
- 外部アクセス前提
- CORS設計必須
SSR:
- 内部通信として扱える
- 非公開APIにできる
- CORS不要
セキュリティの境界が変わる点が重要です。
よくある失敗
SPA移行時に起きやすい問題があります。
- 本番だけCORSエラー
- Cookieが送信されない
- OPTIONSが405になる
原因はAPIサーバがブラウザアクセスを想定していないことです。従来はサーバ間通信しかなかったため、CORSヘッダが実装されていません。
まとめ
CORSはAPIの機能ではなく、ブラウザのセキュリティ制御です。通信主体がブラウザになるSPAでは必須になり、サーバ主体のSSRではほぼ不要になります。
重要なのは「どこが信頼境界か」です。SPAではブラウザが境界、SSRではサーバが境界になります。この違いを理解して設計すると、CORSエラーや認証トラブルを大幅に減らせます。