CORS対応のメモ

NetlifyにデプロイしたアプリからSteamAPI呼んで遊ぼうと思ったら弾かれて悲しい思いしたのでメモ。

仕事で一度引っかかってつらい思いしたのにサックリ対応してなかったから頭に入ってなかったみたいです。
ちゃんと理解して対応しましょうね。

CORSとは

正式名称は”Cross-Origin Resource Sharing”(オリジン間リソース共有)。

ブラウザはセキュリティ上の理由で、自分自身と異なるオリジンからのアクセスを拒否するようになっています。
→もしこれがないとfoo.jpってサイトを開いたのに、知らない内にbar.jpってとこから情報抜かれてた!みたいなことがあり得る

ただ、実際今回のように異なるオリジンのAPIを呼ぶなんて日常茶飯事……
そこで出てくるのがCORS!
HTTPヘッダを使用して異なるオリジン間でもアクセスできるようにしてくれます!!

2種類のCORS対応

CORS対応にはpreflightという事前リクエストが必要なパターンと、必要でないパターンがあります。。
MDNによると、以下の条件を全て満たしていればpreflightリクエストは必要ないらしい。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 許可されているのは以下のメソッドのみです。
- GET
- HEAD
- POST
- ユーザーエージェントによって自動的に設定されたヘッダー (たとえば Connection、 User-Agent、
または Fetch 仕様書で "forbidden header name" として定義されている名前のヘッダー) を除いて、
手動で設定できるヘッダーは、 Fetch 仕様書で "CORS-safelisted request-header" として定義されている以下のヘッダーだけです。
- Accept
- Accept-Language
- Content-Language
- Content-Type (但し、下記の要件を満たすもの)
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
- Content-Type ヘッダーでは以下の値のみが許可されています。
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- リクエストに使用されるどの XMLHttpRequestUpload にもイベントリスナーが登録されていないこと。
これらは正しく XMLHttpRequest.upload を使用してアクセスされます。
- リクエストに ReadableStream オブジェクトが使用されていないこと。

PUTとかDELEAT辺りを使おうとするとpreflightリクエストが必要みたいです。

実際に作って試してみる

今回はとりあえずpreflightが必要ないパターンについてサンプルを作って試してみました。

ソースはこちら

サーバーはexpress、クライアントはVue.jsで作成。
両方ともローカルで試していますが、サーバーを3000ポート、クライアントを8080ポートで立ち上げているためオリジンは別扱いとなります。

CORS対応なし

まずはcors対応なし。

クライアント

callボタンを押すとサーバーにGETリクエストを送り、取得したデータをコンソールに表示します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<button @click="callHello">Call</button>
</div>
</template>
<script>
import Axios from "axios";
export default {
name: "CallApi",
methods: {
async callHello() {
try {
const res = await Axios.get(`http://localhost:3000/api/v1/helloworld`);
console.log(res.data);
} catch (err) {
console.log(err);
}
}
}
};
</script>

サーバー

シンプルにHelloworldの文字列を含むjsonを返すAPIを作成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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

// CORS対応してないパターン
app.get('/api/v1/helloworld', (req, res) => {
res.json({
message: 'Hello world!!!!'
});
});

// 3000番ポートで待ち受け
var server = app.listen(3000, function() {
console.log('Node.js is listening to PORT:' + server.address().port);
});

実行結果

予想通り、クロスオリジンのエラーで弾かれます。

1
2
3
4
5
localhost/:1 Access to XMLHttpRequest at 'http://localhost:3000/api/v1/helloworld' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
CallApi.vue?cbce:17 Error: Network Error
at createError (createError.js?2d83:16)
at XMLHttpRequest.handleError (xhr.js?b50d:87)
xhr.js?b50d:178 Cross-Origin Read Blocking (CORB) blocked cross-origin response http://localhost:3000/api/v1/helloworld with MIME type application/json. See https://www.chromestatus.com/feature/5629709824032768 for more details.

CORS対応あり

クライアント

クライアントは特に変更なく、APiをGETで呼ぶだけです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<button @click="callCorsHello">Call(cors)</button>
</div>
</template>
<script>
import Axios from "axios";
export default {
name: "CallApi",
methods: {
async callCorsHello() {
try {
const res = await Axios.get(`http://localhost:3000/api/v1/corshelloworld`);
console.log(res.data);
} catch (err) {
console.log(err);
}
}
}
};
</script>

サーバー

サーバーは少し実装が異なります。
何をやっているかというと、Access-Control-Allow-Originというヘッダーにリクエストヘッダーから取得したOriginの値を設定しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var express = require('express');
var app = express();

// CORS対応してないパターン
app.get('/api/v1/helloworld', (req, res) => {
res.json({
message: 'Hello world!!!!'
});
});

// CORS対応してるパターン
app.get('/api/v1/corshelloworld', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', req.header('Origin'));
res.json({
message: 'Hello world!!!!'
});
});

// 3000番ポートで待ち受け
var server = app.listen(3000, function() {
console.log('Node.js is listening to PORT:' + server.address().port);
});

実行結果

これで問題なくjsonを受けとることができました!

1
{message: "Hello world!!!!"}

まとめ

preflightが不要なら割と簡単に実装はできそうです。
ただ、対応が必要なのはサーバー側なので、今回最初に試そうとしていたように、Netlifyに配置した静的サイトから直接外部サービスのAPIを呼ぶのは無理そうですね……

lambdaとかからSteamAPIを呼ぶ処理を作り、その処理を静的サイトから呼び出すとかで実装できるんですかね。
試してみないと……