CORSを理解する

# はじめに
フロントエンドとバックエンドを別のドメインで実装する際に必ずと言っていいほど必要となるCORSについて概念から実装方法までまとめた記事になります。

# CORSとは
CORS（Cross-Origin Resource Sharing）は、追加のHTTPヘッダーを使用して、あるオリジンで実行中のWebアプリケーションに、異なるオリジンのリソースへのアクセスを許可するように伝えるブラウザの仕組みです。

## CORSが必要な理由
ブラウザには同一オリジンポリシー（Same-Origin Policy）というセキュリティ機能が組み込まれており、これは、あるオリジンのスクリプトが異なるオリジンのリソースにアクセスすることを制限する仕組みです。

例えば、
`https://frontend.example.com`で動作するWebアプリケーションが
`https://api.example.com`のAPIにアクセスしようとする場合

これは異なるオリジン間の通信となり、デフォルトでは制限されます。

:::note info
### CORSを使用したリクエストの流れ（図解）
```mermaid
sequenceDiagram
participant Frontend as frontend.example.com
participant Browser as ブラウザ
participant Backend as api.example.com

Frontend->>Browser: APIリクエスト
Browser->>Backend: プリフライトリクエスト(OPTIONS)
Backend->>Browser: CORSヘッダーを含むレスポンス
alt CORSヘッダーが適切
    Browser->>Backend: 本来のリクエスト
    Backend->>Browser: レスポンス
    Browser->>Frontend: レスポンス
else CORSヘッダーが不適切
    Browser->>Frontend: エラー
end
```

1. フロントエンドがAPIリクエストを発行
2. ブラウザが自動的にOPTIONSメソッドでプリフライトリクエストを送信
3. バックエンドがCORSヘッダーを返却
4. ヘッダーが適切な場合のみ、本来のリクエストを実行
:::

:::note info
### 同一オリジンポリシーのセキュリティ
同一オリジンポリシーは、以下のような攻撃を防ぎます。
- 悪意のあるサイトによる機密データの窃取
:::



## CORSの構成要素
| ヘッダー名 | 説明 | 例 |
|---|---|---|
|`Access-Control-Allow-Origin`|許可するオリジンを指定|`http://localhost:3000` または `*`
|`Access-Control-Allow-Methods`|許可するHTTPメソッドを指定|`GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS`|
|`Access-Control-Allow-Headers`|許可するHTTPヘッダーを指定|`Content-Type, Accept`|
|`Access-Control-Allow-Credentials`|クレデンシャル（Cookie等）の送信を許可|`true`|
|`Access-Control-Expose-Headers`|クライアントに公開するヘッダーを指定|`Set-Cookie`|
|`Access-Control-Max-Age`|プリフライトリクエストの結果をキャッシュする時間（秒）|`100`|

:::note info
一部のフレームワークやライブラリでは`Access-Control-Allow-Origin`を`*`に設定できないことがあります。
:::


# Goで実装
```go
package main

import (
    "log"
    "net/http"
)

func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // CORSヘッダーの設定
        w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

        // プリフライトリクエストの処理
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusOK)
            return
        }

        next.ServeHTTP(w, r)
    })
}

func main() {
    // APIハンドラーの定義
    apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"message": "Hello, World!"}`))
    })

    // ミドルウェアの適用
    handler := corsMiddleware(apiHandler)

    // サーバーの起動
    log.Printf("Starting server on :8080")
    if err := http.ListenAndServe(":8080", handler); err != nil {
        log.Fatal(err)
    }
}
```
### コード説明
#### 1. corsMiddleware関数
- HTTPハンドラーをラップするミドルウェア関数
- CORSに必要なヘッダーを設定
- OPTIONSメソッドによるプリフライトリクエストを処理


#### 2. main関数
- シンプルなJSONレスポンスを返すAPIハンドラーを定義
- corsMiddlewareを適用
- 8080ポートでサーバーを起動

## テストしてみる
Postmanやcorlコマンドではブラウザを中継しないため、CORSがどう動いているのかを確認できません。
今回はReactを用いて動作のテストをします。
```bash
npx create-react-app cors-test
```

`cors-test/src/App.js`を以下のコードに書き換え
```App.js
import React, { useState } from 'react';

function App() {
  const [response, setResponse] = useState(null);
  const [error, setError] = useState(null);

  const testCORS = async () => {
    try {
      const res = await fetch('http://localhost:8080', {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
      });
      const data = await res.json();
      setResponse(data);
      setError(null);
    } catch (err) {
      setError(err.message);
      setResponse(null);
    }
  };

  return (
    <div style={{ textAlign: 'center', padding: '20px' }}>
      <h1>CORSテスト</h1>
      <button onClick={testCORS}>CORS APIを呼び出す</button>
      {response && <div><h2>レスポンス:</h2><pre>{JSON.stringify(response, null, 2)}</pre></div>}
      {error && <div><h2>エラー:</h2><pre>{error}</pre></div>}
    </div>
  );
}

export default App;
```

```bash
npm start
```
起動すると以下のようなページが見えるはずです。
![スクリーンショット 2024-12-20 16.07.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759397/bc7f67ba-85eb-b88b-0137-c10a8a8793ab.png)

### CORSの設定が正しい時の動作確認
上で説明したgoのコードを実際に動かしてみましょう
main.goに上記のコードを貼り付けて以下のコマンドを実行します
```bash
go run main.go
```
正常に動作するのを確認できたら
フロントのボタン(`CORS APIを呼び出す`)をクリックしてみましょう
正しく動作している場合は以下のようにレスポンスが帰ってきます。
![スクリーンショット 2024-12-20 17.56.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759397/8ff3e1d0-05c4-6b3d-baad-2548b38f6d74.png)

### CORSの設定が誤っている時の動作確認
CORSの許可しているオリジンを変更してみます。
```main.go
func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 許可するオリジンをlocalhost:3001に変更
        w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3001")
        
        // 省略
    
    })
}
```
以下のようにエラーが表示されます。
![スクリーンショット 2024-12-20 22.03.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759397/dfd160e3-7195-a861-81fa-bd432ae132e5.png)
ブラウザのコンソールを見るとオリジンが許可されているものが違うというエラーが見て取れますね。フロントエンドの開発をしている際、このようなエラーが出ていればCORSを疑ってみてください。
![スクリーンショット 2024-12-20 22.04.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759397/5c02e913-6582-b9b6-f24c-158cd18b02b7.png)


# 参考
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS