expressでCORSエラーが起きたらcorsで解決しよう

4 min読了の目安(約4100字TECH技術記事

始め

すべての始まりは一つのエラーでした。

Access to XMLHttpRequest at 'http://localhost:3065/user' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

ですので、今回はこの話をします。

1. CORS

まずCORSについてからです。

CORSは「Cross-origin resource sharing」の略です。直訳したら「Cross-originリソース共有」ぐらいですかね。このCross-originが何かわからなかったので、調べました。

URLの構成を簡単に絵で表してみました。色々ありますが、その中でProtocol、Host、Port Numberの3つを合わせた部分がoriginです。

この3つが同じだったら同じオリジン(same-origin)、この3つの中で一つでも違ったら違うオリジン(cross-origin)です。つまり、CORSは違うオリジン同士でもリソースを共有することという意味でしょう。

もっと正確な定義はMDNの説明を見てみましょう。

追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組み

2. CORSとSOP

ウェブの世界では違うオリジン同士のリソース共有に関するポリシーが2つあります。1つはつい先説明したCORS、もう1つはSOPです。

SOPはSame-Origin Policyの略で、文字通りに同じオリジン同士だけリソース共有ができるというポリシーです。

しかし、ウェブ世界で違うオリジンからリソースを持ってくることは頻繁に起こること(外部API使用など)なので完全に制限することは難しいです。ですのでいくつかの例外をおいて、その例外に該当する場合は違うオリジンでもアクセスを許可しています。

その例外の中で1つがCORSポリシーを守るリソースリクエストです。

Generally, reading information from another origin is forbidden. However, an origin is permitted to use some kinds of resources
...
Network resources can also opt into letting other origins read their information, for example, using Cross-Origin Resource Sharing [CORS]. In these cases, access is typically granted on a per-origin basis.
RFC 6454 - 3.4.2 Network Accessから

違うオリジンにリソースリクエストを送ったらSOP違反になるし、例外のCORSポリシーも守らなかったら違うオリジンからのリソースは使えなくなるということです。

正直めんどうくさいですが、こういうセキュリティポリシーがなかったら誰もかも私のウェブサイトにアクセスして好き勝手に暴れるかもしれないので大人しく従うしかありません。

3. エラーの原因

さて、最初にお見せしたエラーの原因を話しましょう。これはブラウザからバックエンドサーバーにリクエストを送ったときに起こったエラーです。内容をよく見たら「http://localhost:3000からhttp://localhost:3065/userへ送ったアクセスがCORSポリシーによってブロックされた」と書いてます。

気づきましたか?そうです、ポート番号が違います

クライアント側のポート番号が3000でバックエンド側のポート番号が3065なので違うオリジンですね。ここで既にSOP違反してるのにCORSポリシーを守る処理も入れてないからブロックされちゃったというシナリオでしょう。

🤔 リクエストはなぜいけましたか?
CORSポリシーはブラウザに関するポリシーです。ブラウザはオリジンを比較して違ったらブロックします。ですが、サーバーは別にそんなこと関係無しでオリジンが違っても正常処理をします。同じ理由でサーバー同士にアクセスする時はオリジンが違ってもCORSエラーは起きません。

4. 解決: ヘッダー追加

それではCORSポリシーを守る処理を入れてみましょう。どうすればいいかはエラーメッセージが親切に教えてくれてます。

No 'Access-Control-Allow-Origin' header is present on the requested resource

リクエストされたリソースにAccess-Control-Allow-Originヘッダーがないと怒ってるので、それを入れれば良いです。

router.get('/', (req, res) => {
    try {
	res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000')
        res.status(201).send('ok')
    } catch (error) {
        console.error(error)
    }
})

超簡単サンプルコードです。setHeaderAccess-Control-Allow-Originヘッダーを追加、http://localhost:3000という値を入れました。こうするとhttp://localhost:3000からのアクセスは許可するという意味になります。

ちなみにhttp://localhost:3000の代わりに*を書いたらすべてのオリジンからアクセスを許可する意味になります。開発してる時は楽でしょうが、セキュリティ的に危ないのでやめたほうが良いと思います。

このように直接ヘッダーを追加する方法だとすべてのレスポンスロジックにこのコードをいちいち書かないといけません。めんどうくさいです。

5. 解決: ミドルウェア

expressならミドルウェアでCORS設定したほうが楽だと思います。そのミドルウェアの名前もcorsです(紛らわしい)。npm install corsコマンドで簡単にインストールできます。

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

app.use(cors());

corsをインストールしてapp.jsに上記のコードを追加したらレスポンスロジックにいちいち書かなくても済むので楽になりました。

ですが、cors()のように関数の中を空にしたらこれもすべてのオリジンからアクセスを許可する意味になるので危険です。ディテールは設定する必要があります。

app.use(cors({
    origin: 'http://localhost:3000', //アクセス許可するオリジン
    credentials: true, //レスポンスヘッダーにAccess-Control-Allow-Credentials追加
    optionsSuccessStatus: 200 //レスポンスstatusを200に設定
}))

などなどの設定ができます。もっと多様な設定は公式ドキュメントをご参考ください。

私はこれでCORSエラーから開放されました。

終わり

今回はめちゃくちゃ簡単にまとめましたが、これも調べたら無限に内容が出てくるので勉強になりました。CORS解決案もこれ以外にProxyとか色々あります。

私は最初に「バックエンドサーバーも同じポート番号使えばいいじゃない?」と思ってクライアントと同じく3000に修正してみたら早速Port 3000 is already in use.と怒られました。