フロントエンドとバックエンドの接続(CORS設定)

2025-08-29

はじめに

前回までで、JWT認証を含む完全なバックエンドAPIとReactフロントエンドを実装しました。しかし、実際に両者を別々のドメインで実行しようとすると、CORS(Cross-Origin Resource Sharing)の問題に直面します。今回は、この重要なセキュリティメカニズムであるCORSを理解し、適切に設定する方法を詳しく学びます。

CORSとは何か?

CORSは、ブラウザが異なるオリジン(ドメイン、プロトコル、ポート)間でリソースを安全に共有できるようにするメカニズムです。現代のブラウザはセキュリティ上の理由から、同一オリジンポリシー(Same-Origin Policy)を適用しており、これがクロスオリジンリクエストを制限しています。

同一オリジンポリシーとCORSの関係

[図解: 同一オリジンポリシーとCORS]
ブラウザ → 同一オリジンリクエスト: ✅ 許可
ブラウザ → クロスオリジンリクエスト: ❌ ブロック(CORSヘッダーがない場合)
ブラウザ → クロスオリジンリクエスト: ✅ 許可(適切なCORSヘッダーがある場合)

クロスオリジンリクエストの例

  • フロントエンド: http://localhost:3000 (React開発サーバー)
  • バックエンド: http://localhost:5000 (Expressサーバー)

これらはポート番号が異なるため、異なるオリジンと見なされ、CORS設定が必要です。

CORSの動作原理

CORSはプリフライトリクエスト(Preflight Request)と実際のリクエストの2段階で動作します。

1. プリフライトリクエスト

複雑なリクエスト(POST、PUT、DELETEなど)の前に、OPTIONSメソッドで安全性を確認します。

[図解: プリフライトフロー]
1. ブラウザ → OPTIONSリクエスト → サーバー
2. ブラウザ ← CORSヘッダーを含む応答 ← サーバー
3. ブラウザが許可を確認 → 実際のリクエストを送信

2. 実際のリクエスト

プリフライトが成功した後、実際のリクエストが送信されます。

Express.jsでのCORS設定

1. corsパッケージのインストール

npm install cors
npm install -D @types/cors

2. 基本的なCORS設定

// server.js
const express = require('express');
const cors = require('cors');
require('dotenv').config();

const authRoutes = require('./src/routes/auth');
const todoRoutes = require('./src/routes/todos');

const app = express();
const port = process.env.PORT || 5000;

// 基本的なCORS設定(すべてのオリジンを許可)
app.use(cors());

// または、より制限的な設定
app.use(cors({
  origin: 'http://localhost:3000', // フロントエンドのURL
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true // クッキーや認証情報の送信を許可
}));

// ミドルウェアの順序に注意: CORSの後にJSONパーサーを設定
app.use(express.json());

// ルートの設定
app.use('/api/auth', authRoutes);
app.use('/api/todos', todoRoutes);

// 健康状態チェック用エンドポイント
app.get('/api/health', (req, res) => {
  res.json({ 
    status: 'OK', 
    timestamp: new Date().toISOString(),
    environment: process.env.NODE_ENV || 'development'
  });
});

// 404ハンドリング
app.use('/api/*', (req, res) => {
  res.status(404).json({ error: 'エンドポイントが見つかりません' });
});

// エラーハンドリングミドルウェア
app.use((error, req, res, next) => {
  console.error('予期せぬエラー:', error);
  res.status(500).json({ error: 'サーバー内部エラーが発生しました' });
});

app.listen(port, () => {
  console.log(`サーバーが起動しました: http://localhost:${port}`);
  console.log(`CORS設定: フロントエンドからのリクエストを許可しています`);
});

3. 環境に応じた動的CORS設定

実際のアプリケーションでは、開発環境と本番環境で異なるCORS設定が必要です。

// src/config/cors.js
const whitelist = [
  'http://localhost:3000', // 開発環境
  'http://localhost:3001',
  'https://yourproductiondomain.com', // 本番環境
  'https://www.yourproductiondomain.com'
];

const corsOptions = {
  origin: function (origin, callback) {
    // 開発環境(Postmanなど)ではoriginがundefinedになるため許可
    if (!origin || whitelist.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      callback(new Error('CORSポリシーによってブロックされました'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
  allowedHeaders: [
    'Content-Type', 
    'Authorization', 
    'X-Requested-With',
    'Accept',
    'Origin'
  ],
  exposedHeaders: ['Authorization', 'Content-Length', 'X-Foo', 'X-Bar'],
  credentials: true,
  maxAge: 86400, // プリフライトリクエストの結果をキャッシュする時間(秒)
  preflightContinue: false,
  optionsSuccessStatus: 204
};

module.exports = corsOptions;

4. 動的CORS設定の適用

// server.js(更新版)
const express = require('express');
const cors = require('cors');
require('dotenv').config();

const corsOptions = require('./src/config/cors');
const authRoutes = require('./src/routes/auth');
const todoRoutes = require('./src/routes/todos');

const app = express();
const port = process.env.PORT || 5000;

// 動的CORS設定の適用
app.use(cors(corsOptions));

// プリフライトリクエストの明示的な処理
app.options('*', cors(corsOptions)); // すべてのルートでOPTIONSを許可

// ミドルウェアの順序に注意
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// ルートの設定
app.use('/api/auth', authRoutes);
app.use('/api/todos', todoRoutes);

// カスタムCORSミドルウェア(詳細なロギング用)
app.use((req, res, next) => {
  const origin = req.headers.origin;
  console.log(`受信リクエスト: ${req.method} ${req.path} from origin: ${origin}`);
  next();
});

// 健康状態チェック
app.get('/api/health', (req, res) => {
  res.json({ 
    status: 'OK', 
    timestamp: new Date().toISOString(),
    cors: {
      allowedOrigins: corsOptions.origin.toString(),
      environment: process.env.NODE_ENV || 'development'
    }
  });
});

// 404ハンドリング
app.use('/api/*', (req, res) => {
  res.status(404).json({ error: 'エンドポイントが見つかりません' });
});

// エラーハンドリング(CORSエラーも含む)
app.use((error, req, res, next) => {
  if (error.message.includes('CORS')) {
    console.warn('CORSエラー:', error.message);
    return res.status(403).json({ 
      error: 'クロスオリジンリクエストが許可されていません',
      details: process.env.NODE_ENV === 'development' ? error.message : undefined
    });
  }

  console.error('予期せぬエラー:', error);
  res.status(500).json({ 
    error: 'サーバー内部エラーが発生しました',
    ...(process.env.NODE_ENV === 'development' && { details: error.message })
  });
});

app.listen(port, () => {
  console.log(`サーバーが起動しました: http://localhost:${port}`);
  console.log('CORS設定:');
  console.log('- 許可されているオリジン:', whitelist);
  console.log('- 許可されているメソッド:', corsOptions.methods);
  console.log('- 環境:', process.env.NODE_ENV || 'development');
});

フロントエンドの設定調整

1. 環境変数の設定

Reactアプリケーションで環境変数を設定して、APIのベースURLを管理します。

// .env.development
REACT_APP_API_URL=http://localhost:5000
REACT_APP_ENVIRONMENT=development

// .env.production
REACT_APP_API_URL=https://api.yourdomain.com
REACT_APP_ENVIRONMENT=production

2. APIクライアントの更新

// src/api/client.js(更新版)
import axios from 'axios';

// 環境に応じたAPIベースURLの設定
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000';

console.log(`APIベースURL: ${API_BASE_URL}`);
console.log(`環境: ${process.env.REACT_APP_ENVIRONMENT}`);

const apiClient = axios.create({
  baseURL: `${API_BASE_URL}/api`,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// リクエストインターセプター - トークンとCORS関連ヘッダー
apiClient.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('authToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }

    // CORS関連のヘッダー(必要に応じて)
    config.headers['X-Requested-With'] = 'XMLHttpRequest';

    console.log(`送信リクエスト: ${config.method?.toUpperCase()} ${config.url}`);
    return config;
  },
  (error) => {
    console.error('リクエスト設定エラー:', error);
    return Promise.reject(error);
  }
);

// レスポンスインターセプター - エラーハンドリング
apiClient.interceptors.response.use(
  (response) => {
    console.log(`受信レスポンス: ${response.status} ${response.config.url}`);
    return response;
  },
  (error) => {
    console.error('APIレスポンスエラー:', error.response?.status, error.message);

    if (error.response?.status === 401) {
      // 認証エラー
      localStorage.removeItem('authToken');
      localStorage.removeItem('user');
      window.location.href = '/login';
    } else if (error.response?.status === 403) {
      // CORSエラーや権限不足
      console.error('アクセスが拒否されました。CORS設定を確認してください。');
    } else if (error.code === 'NETWORK_ERROR') {
      // ネットワークエラー(サーバーに到達できない)
      console.error('ネットワークエラー: サーバーに接続できません');
    } else if (error.code === 'ECONNABORTED') {
      // タイムアウト
      console.error('リクエストがタイムアウトしました');
    }

    return Promise.reject(error);
  }
);

export default apiClient;

3. プロキシ設定(開発環境用)

Create React Appでは、開発環境でプロキシを設定することでCORS問題を回避できます。

// package.json(フロントエンド)
{
  "name": "todo-frontend",
  "version": "0.1.0",
  "private": true,
  "proxy": "http://localhost:5000", // バックエンドのURL
  "dependencies": {
    // ...
  }
}

プロキシ設定後、APIリクエストは相対パスで送信できます:

// プロキシ設定後はベースURLを省略可能
const response = await axios.get('/api/todos');

よくあるCORSエラーと解決策

1. “Access-Control-Allow-Origin” ヘッダーがない

原因: サーバーが適切なCORSヘッダーを返していない
解決策: cors()ミドルウェアを正しく設定する

2. 資格情報付きリクエストのエラー

原因: withCredentialsがtrueだが、サーバーがAccess-Control-Allow-Credentials: trueを返していない
解決策:

app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true
}));

3. プリフライトリクエストの失敗

原因: OPTIONSメソッドが適切に処理されていない
解決策:

app.options('*', cors()); // すべてのOPTIONSリクエストを処理

4. 許可されていないヘッダーのエラー

原因: カスタムヘッダーがAccess-Control-Allow-Headersに含まれていない
解決策:

app.use(cors({
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header']
}));

本番環境でのCORS設定

本番環境では、より厳格なCORSポリシーを設定します。

// src/config/cors.production.js
const productionWhitelist = [
  'https://yourdomain.com',
  'https://www.yourdomain.com',
  'https://yourapp.herokuapp.com'
];

const productionCorsOptions = {
  origin: function (origin, callback) {
    // オリジンがundefined(サーバーサイドリクエスト)または許可リストにある場合
    if (!origin || productionWhitelist.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      console.warn(`CORSブロック: ${origin}は許可されていません`);
      callback(new Error('許可されていないオリジンです'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 600 // 本番環境では短めのキャッシュ時間
};

module.exports = productionCorsOptions;

テストと検証

1. CORS設定のテストエンドポイント

// テスト用ルートの追加
app.get('/api/cors-test', (req, res) => {
  const origin = req.headers.origin;
  res.json({
    message: 'CORSテスト成功',
    yourOrigin: origin,
    timestamp: new Date().toISOString(),
    corsEnabled: true,
    allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
  });
});

2. curlコマンドでのテスト

# プリフライトリクエストのテスト
curl -X OPTIONS http://localhost:5000/api/todos \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization" \
  -I

# 実際のリクエストのテスト
curl -X GET http://localhost:5000/api/health \
  -H "Origin: http://localhost:3000" \
  -I

次のステップ

CORSの適切な設定により、フロントエンドとバックエンドの安全な接続が確保できました。次回は、これまで学んだすべての技術を統合し、データベース連携された完全なTODOアプリケーションの完成版を作成します。これには、ユーザー認証、データ永続化、適切なエラーハンドリングなど、本格的なWebアプリケーションに必要なすべての要素が含まれます。

まとめ

CORSの適切な設定は、現代のWebアプリケーション開発において不可欠です。今回学んだ主要内容:

  1. CORSの基本概念: 同一オリジンポリシーとその重要性
  2. プリフライトメカニズム: 複雑なリクエストの安全性確認プロセス
  3. Express.jsでの実装: corsミドルウェアの適切な設定方法
  4. 環境別設定: 開発環境と本番環境での適切なCORSポリシー
  5. よくある問題と解決策: 実際の開発で遭遇するCORSエラーの対処法

これらの知識により、安全かつ効率的なクロスオリジン通信を実現できるようになりました。これで、フロントエンドとバックエンドの連携に関する主要な課題はすべて解決し、完全なアプリケーションの完成に向けて準備が整いました。