1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cross-Origin エラーを完全に理解する

Posted at

こんにちは!Web開発をしていて「Access to fetch at '...' has been blocked by CORS policy」というエラーに遭遇したことはありませんか?私も最初はこのエラーに悩まされました。今回は、このCross-Origin(CORS)エラーについて、その仕組みから解決方法まで徹底的に解説します!

はじめに:CORSエラーとの初遭遇

多くの開発者が最初に遭遇するのは、こんなシナリオです:

// フロントエンド(localhost:3000)から
fetch('http://localhost:8080/api/users')
  .then(response => response.json())
  .then(data => console.log(data));

結果

Access to fetch at 'http://localhost:8080/api/users' from origin 'http://localhost:3000' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present 
on the requested resource.

「え?ローカル環境なのになぜ?」と思った方、安心してください。これは正常な動作です!

そもそもCORS(Cross-Origin Resource Sharing)とは?

Same-Origin Policy(同一オリジンポリシー)

まず理解すべきは、ブラウザの基本的なセキュリティ機能である「同一オリジンポリシー」です。

オリジンの構成要素

  • プロトコル(http/https)
  • ホスト(example.com)
  • ポート番号(80, 443, 3000など)
// 同一オリジンの例
// 基準: https://example.com:443/page1

https://example.com/page2           // ✅ 同一オリジン
https://example.com:443/api/users   // ✅ 同一オリジン(デフォルトポート)

// 異なるオリジンの例
http://example.com/api              // ❌ プロトコルが違う
https://api.example.com/users       // ❌ サブドメインが違う
https://example.com:8080/api        // ❌ ポート番号が違う

CORSの役割

CORSは、この厳格な同一オリジンポリシーを安全に緩和するための仕組みです。サーバー側が「この外部オリジンからのアクセスを許可します」と明示的に宣言することで、クロスオリジンリクエストを可能にします。

CORSエラーが発生する仕組み

1. シンプルリクエストの場合

// シンプルリクエストの例
fetch('https://api.example.com/data', {
  method: 'GET',
  headers: {
    'Content-Type': 'text/plain'
  }
});

ブラウザの動作

  1. リクエストを送信
  2. レスポンスを受信
  3. Access-Control-Allow-Originヘッダーをチェック
  4. ヘッダーがない、または現在のオリジンが含まれていない場合、エラー

2. プリフライトリクエストの場合

// プリフライトが必要なリクエストの例
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  body: JSON.stringify({ name: 'John' })
});

ブラウザの動作

  1. OPTIONSメソッドでプリフライトリクエストを送信
  2. サーバーからのCORSヘッダーを確認
  3. 許可されていれば、実際のリクエストを送信
  4. 許可されていなければ、エラーで止まる

よくあるCORSエラーパターン集

パターン1: Access-Control-Allow-Originヘッダーなし

Access to fetch at 'http://api.example.com/data' from origin 'http://localhost:3000' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present 
on the requested resource.

原因: サーバーがCORSヘッダーを設定していない

パターン2: オリジンの不一致

Access to fetch at 'http://api.example.com/data' from origin 'http://localhost:3000' 
has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 
'http://localhost:8080' that is not equal to the supplied origin.

原因: サーバーで設定されたオリジンと実際のオリジンが異なる

パターン3: プリフライトリクエストの失敗

Access to fetch at 'http://api.example.com/data' from origin 'http://localhost:3000' 
has been blocked by CORS policy: Method POST is not allowed by Access-Control-Allow-Methods 
in response to a preflight request.

原因: プリフライトでPOSTメソッドが許可されていない

パターン4: カスタムヘッダーの拒否

Access to fetch at 'http://api.example.com/data' from origin 'http://localhost:3000' 
has been blocked by CORS policy: Request header field authorization is not allowed by 
Access-Control-Allow-Headers in response to a preflight request.

原因: カスタムヘッダー(Authorization等)が許可されていない

サーバー側での解決方法

1. Express.js での設定

基本的な設定

const express = require('express');
const app = express();

// 基本的なCORS設定
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  
  // プリフライトリクエストへの対応
  if (req.method === 'OPTIONS') {
    res.sendStatus(200);
  } else {
    next();
  }
});

corsライブラリを使用

const cors = require('cors');

// 全てのオリジンを許可(開発環境のみ)
app.use(cors());

// 特定のオリジンのみ許可(本番環境推奨)
app.use(cors({
  origin: ['http://localhost:3000', 'https://myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true  // Cookieを含む場合
}));

2. Next.js API Routes での設定

// pages/api/users.js または app/api/users/route.js
export default function handler(req, res) {
  // CORS ヘッダーを設定
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

  if (req.method === 'OPTIONS') {
    res.status(200).end();
    return;
  }

  // 実際のAPI処理
  res.status(200).json({ message: 'Hello World' });
}

3. Python Flask での設定

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)

# 基本的な設定
CORS(app)

# 詳細な設定
CORS(app, 
     origins=['http://localhost:3000', 'https://myapp.com'],
     methods=['GET', 'POST', 'PUT', 'DELETE'],
     allow_headers=['Content-Type', 'Authorization'])

@app.route('/api/users')
def get_users():
    return {'users': []}

4. Spring Boot での設定

@RestController
@CrossOrigin(origins = "http://localhost:3000")  // 特定オリジン
public class UserController {
    
    @GetMapping("/api/users")
    public List<User> getUsers() {
        return userService.getAllUsers();
    }
}

// グローバル設定
@Configuration
public class CorsConfig {
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

フロントエンド側での対処法

1. プロキシの使用(開発環境)

Vite での設定

// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

Create React App での設定

/package.json

{
  "name": "my-app",
  "version": "0.1.0",
  "proxy": "http://localhost:8080"
}

Next.js での設定

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'http://localhost:8080/api/:path*'
      }
    ]
  }
}

2. リクエストの調整

シンプルリクエストにする

// ❌ プリフライトが必要
fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'  // カスタムヘッダー
  },
  body: JSON.stringify(data)
});

// ✅ シンプルリクエスト
fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  body: new URLSearchParams(data)
});

環境別の対策戦略

開発環境

  1. プロキシを使用(最も簡単)
  2. サーバー側でワイルドカード許可
  3. ブラウザのセキュリティを無効化(非推奨)
# Chrome でセキュリティを無効化(開発時のみ)
google-chrome --disable-web-security --user-data-dir="/tmp/chrome_dev_session"

ステージング/本番環境

  1. 特定のオリジンのみ許可
  2. 必要最小限のメソッド・ヘッダーのみ許可
  3. 環境変数でオリジンを管理
// 環境に応じたオリジン設定
const allowedOrigins = process.env.NODE_ENV === 'production' 
  ? ['https://myapp.com', 'https://www.myapp.com']
  : ['http://localhost:3000', 'http://localhost:3001'];

app.use(cors({
  origin: allowedOrigins,
  credentials: true
}));

よくある誤解と落とし穴

誤解1: 「CORSはサーバー側の問題」

真実: CORSはブラウザのセキュリティ機能です。PostmanやcURLでは発生しません。

# このコマンドは成功する(ブラウザを経由しないため)
curl -X GET http://api.example.com/users

誤解2: 「Access-Control-Allow-Origin: *で全て解決」

問題: 認証情報(Cookies、Authorization header)を含むリクエストでは*が使えません。

// ❌ エラーになる
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Credentials', 'true');

// ✅ 正しい
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
res.header('Access-Control-Allow-Credentials', 'true');

誤解3: 「HTTPSからHTTPへのリクエストはCORSで解決」

真実: Mixed Content Policy により、HTTPSからHTTPへのリクエストはCORSとは別の理由でブロックされます。

デバッグ方法とツール

1. ブラウザの開発者ツール

// Network タブでの確認ポイント
// 1. プリフライトリクエスト(OPTIONS)の有無
// 2. レスポンスヘッダーの確認
// 3. ステータスコード(200でもCORSエラーは発生する)

2. カスタムデバッグ関数

function debugCORS(url, options = {}) {
  console.log('🚀 Sending request to:', url);
  console.log('📦 Request options:', options);
  
  return fetch(url, options)
    .then(response => {
      console.log('✅ Response status:', response.status);
      console.log('📋 Response headers:');
      for (let [key, value] of response.headers.entries()) {
        console.log(`  ${key}: ${value}`);
      }
      return response;
    })
    .catch(error => {
      console.error('❌ CORS Error:', error.message);
      throw error;
    });
}

// 使用例
debugCORS('http://api.example.com/users')
  .then(response => response.json())
  .then(data => console.log(data));

3. プリフライトリクエストの確認

// プリフライトが送信されるかチェック
function willTriggerPreflight(method, headers) {
  const simpleMethod = ['GET', 'HEAD', 'POST'].includes(method);
  const simpleHeaders = Object.keys(headers).every(header => 
    ['accept', 'accept-language', 'content-language', 'content-type'].includes(header.toLowerCase())
  );
  const simpleContentType = !headers['content-type'] || 
    ['application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain'].includes(headers['content-type']);
  
  return !(simpleMethod && simpleHeaders && simpleContentType);
}

console.log(willTriggerPreflight('POST', {'Content-Type': 'application/json'})); // true

セキュリティのベストプラクティス

1. 最小権限の原則

// ❌ 過度に緩い設定
app.use(cors({
  origin: '*',
  methods: '*',
  allowedHeaders: '*'
}));

// ✅ 必要最小限の設定
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
  methods: ['GET', 'POST'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400  // プリフライトのキャッシュ時間
}));

2. 環境別の設定管理

// config/cors.js
const corsConfig = {
  development: {
    origin: ['http://localhost:3000', 'http://localhost:3001'],
    credentials: true
  },
  production: {
    origin: process.env.ALLOWED_ORIGINS.split(','),
    credentials: true,
    optionsSuccessStatus: 200
  }
};

module.exports = corsConfig[process.env.NODE_ENV] || corsConfig.development;

3. 動的オリジン検証

app.use(cors({
  origin: (origin, callback) => {
    // オリジンなし(same-origin)またはモバイルアプリは許可
    if (!origin) return callback(null, true);
    
    // 許可リストとの照合
    const allowedOrigins = process.env.ALLOWED_ORIGINS.split(',');
    if (allowedOrigins.includes(origin)) {
      return callback(null, true);
    }
    
    // 開発環境でのローカルホスト許可
    if (process.env.NODE_ENV === 'development' && origin.includes('localhost')) {
      return callback(null, true);
    }
    
    callback(new Error('Not allowed by CORS'));
  }
}));

トラブルシューティングチェックリスト

サーバー側チェック項目

  • Access-Control-Allow-Originヘッダーが設定されているか
  • オリジンの値が正確か(末尾のスラッシュに注意)
  • Access-Control-Allow-Methodsに必要なメソッドが含まれているか
  • Access-Control-Allow-Headersに必要なヘッダーが含まれているか
  • OPTIONSリクエストに正しく応答しているか
  • 認証情報を使用する場合、Access-Control-Allow-Credentials: trueが設定されているか

フロントエンド側チェック項目

  • リクエストURL が正しいか
  • プロトコル、ホスト、ポートが正確か
  • 必要以上にカスタムヘッダーを送信していないか
  • Content-Typeが適切か
  • プロキシ設定が正しいか(開発環境)

ブラウザ・環境チェック項目

  • ブラウザのキャッシュをクリアしたか
  • 複数のブラウザで確認したか
  • HTTPSとHTTPの混在がないか
  • ネットワークタブでプリフライトリクエストを確認したか

まとめ

CORSエラーは最初は複雑に見えますが、仕組みを理解すれば適切に対処できます。重要なポイントは:

  1. CORSはブラウザのセキュリティ機能であり、サーバー側で適切に設定する必要がある
  2. 開発環境では緩く、本番環境では厳しく設定する
  3. 最小権限の原則に従って、必要最小限の権限のみ許可する
  4. 環境別の設定管理を適切に行う

この記事が皆さんのCORSエラー解決の助けになれば嬉しいです!何か質問やつまずいた点があれば、コメントで教えてください。

参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?