【プリフライトリクエスト編】クロスドメイン関連の挙動をJavaScriptで確認する
前にクロスドメインアクセスのシンプルリクエストの実験はしたんですが、プリフライトリクエストに関してはしてませんでした。
てか正確にはしてたんですが、案の定?CORSのエラーがクリアできなくて諦めただけです。。
今回はその実験が成功したので、書き残しておきます。
前の記事のコードがひどいなとも思ったので、その補足も少しします。💦
プリフライトリクエストってどんな時に送られる?
CORSに関しては、以下に書いたというか参考になる記事を載せたので、書きません。そっちをご確認くださいませ。
そういえばシンプルリクエストがどういう場合に送られるのかっていうのを書いてなかったので、それを少し書くことにします。
プリフライトリクエスト の説明じゃないの?って思うかもですが、シンプルリクエストに当てはまらないリクエストが、プリフライトリクエストになる のだそうです。
でシンプルリクエストってのは、以下に当てはまるリクエストのことをいうらしいです。
・許可されているメソッドのうちの一つであること。
・GET
・HEAD
・POST
・ユーザーエージェントによって自動的に設定されたヘッダー (たとえば Connection、 User-Agent、 または Fetch 仕様書で「禁止ヘッダー名」として定義されているヘッダー) を除いて、手動で設定できるヘッダーは、 Fetch 仕様書で「CORS セーフリストリクエストヘッダー」として定義されている以下のヘッダーだけです。
・Accept
・Accept-Language
・Content-Language
・Content-Type (但し、下記の要件を満たすもの)
・application/x-www-form-urlencoded
・multipart/form-data
・text/plain
・DPR
・Downlink
・Save-Data
・Viewport-Width
・Width
・リクエストに使用されるどの XMLHttpRequestUpload にもイベントリスナーが登録されていないこと。これらは正しく XMLHttpRequest.upload を使用してアクセスされます。
・リクエストに ReadableStream オブジェクトが使用されていないこと。
MDN Web Docsの説明に書いてることそのまんまです。その説明が一番詳しかった。
太字のとこを中心に覚えれば良さそう。
ヘッダいっぱいあるけど、とりあえずapplication/jsonがこのリストにない(=application/jsonを使うとプリフライトリクエスト になる)ってことが大事な気がした。
ちなみにMDN Web Docsは日本語もちゃんとあるけど、たまに訳が変なので意味わからんって思ったら英語を読むといいよ。
シンプルリクエストとは何か、プリフライトリクエストとは何かに関しては、こちらを熟読すべしと思う。
【前提整理】実際にCORSでクロスドメインアクセス
早速実装。説明は基本下手なので、それは他の方に任せて実装をする。
の前に、何を確認したいんだったか整理します。
前と同じく、こちらの記事を参考に実装する。ただしサーバ側のコードはJavaScript。
確認したいことは以下。今回は2つ。
1. CORS許可してないAPIをContent-Typeをapplication/jsonでPOSTリクエストする
→CORSとかプリフライトリクエストに関するエラーが出て失敗するはず
2. CORS許可しているAPIをContent-Typeをapplication/jsonでPOSTリクエストする
→プリフライトリクエストが送られるはず(つまりOPTIONSメソッドとPOSTメソッドのリクエストが飛ぶ)
今回も前と同じく、ローカルサーバを別ポートで2つ起動して確認します。
クライアントのホスト:http://localhost:8000
サーバのホスト:http://localhost:3000
最近やっと、ngrokというlocalhostを一時的に外部公開してくれる便利サービスを使い始めたけど、
今回は使わなくても実験できるし、あとngrokだとURLが起動する度に変わるのでコードの書き換えしないといけないのが面倒なので使わない。
ポート番号違ってたら別オリジンなので、実験レベルでは問題ないかな。
【サーバー側実装】実際にCORSでクロスドメインアクセス
node.jsとexpressで作ります。
corsモジュールは使いません。
きっとはこいつは便利なので、これがなくてもCORS対応の実装した方がCORSの理解が深まるかもしれないという、なんの確証もない考えがあるので使ってないだけです。
きっと実務ではcorsモジュール使うことの方が多いのかなという、これまたなんの確証もない思いもあるけど、まあいいや。
// ライブラリの読み込み
const express = require("express")
const app = express()
// 3000ポートで起動する
const PORT = process.env.PORT || 3000
const cors = (req, res, next) => {
const origin = req.headers.origin
const ALLOWED_ORIGINS = ["http://localhost:8000"]
const ALLOWED_METHODS = ["POST", "OPTIONS"]
const ALLOWED_HEADERS = ["Content-Type"]
if (ALLOWED_ORIGINS.indexOf(origin) > -1) {
res.header("Access-Control-Allow-Origin", origin)
res.header("Access-Control-Allow-Methods", ALLOWED_METHODS.join(","))
res.header("Access-Control-Allow-Headers", ALLOWED_HEADERS.join(","))
}
next()
}
// body-parserの設定
app.use(express.json())
// CORSを許可していないGETのAPI
app.post("/api/v1/ng_cors", function (req, res) {
res.json({
message: "CORS許可してないよ",
})
})
// /api/v1/ok_corsのみcorsを許可する
app.use("/api/v1/ok_cors", cors)
// プリフライトリクエストのOPTIONSメソッドでのリクエストを受ける
app.options("/api/v1/ok_cors", (req, res) => {
res.sendStatus(204)
})
// プリフライトリクエストの実際のリクエストを受ける
app.post("/api/v1/ok_cors", (req, res) => {
const resBody = {
message: "プリフライトリクエストを受け付けた",
}
res.json(resBody)
})
app.listen(PORT, () => {
console.log(`listening on port ${PORT}`)
})
前のコードはとりあえずCORSが許可されてればいいやくらいしか、考えてなくて色んなところからコピペしたひどいコードだったので、ほんの少しだけ改善。。
少しだけです。
ちなみにroutingするみたいなファイル分割が発生することは敢えてやってません。。
例えば、bodyParserの読み込みはやめました。
やめましたというか、いつからかのアップデートからexpressに同機能が標準搭載されるようになったらしいです。
あと「Access-Control-Allow-Origin」にワイルドカードの「*」を指定するのはやめました。。実際*にするとなんでも許可して危なそうなので。。😨
あと前は「Access-Control-Allow-Method」とか「Access-Control-Allow-Headers」は結構適当に設定してて、必要ない値もあったので今回は必要な値だけに絞りました。
あとはCORSの設定をします。
corsの設定は外だしして、「app.use("/api/v1/ok_cors", cors)」のところで「/api/v1/ok_cors」のAPIのみCORS許可してます。
ではnode app.jsで8000ポートで起動します。
POSTMANでまずPOSTで叩いてみると、どっちも200OK返ってきた。
まあCORS対応できているかはこれだとわからないけど、とりあえずCORS以外の問題はなさそう。
ついでにOPTIONSメソッドで2つを叩いてみると、
未対応の方は200OKで、POSTが返ってきた。
対応済みAPIは204 No Contentで返ってきた。
※ブラウザからこのAPIを叩く場合、OPTIONSメソッドによる事前リクエストはブラウザの判断で自動的に発行するので、開発者がOPTIONSメソッドによるリクエストを発行する必要はないです。
ただ、CORSはブラウザの仕組みなので、POSTMANやcurlコマンドでリクエストした場合はOPTIONSメソッドのリクエストは自動発行されないため、POSTMANやcurlでOPTIONSメソッドのレスポンスを確認したい時は自分でOPTIONSメソッドのリクエストを発行する必要があります。
【クライアント側実装】実際にCORSでクロスドメインアクセス
次クライアント。
前と同じくボタンを2つ用意して、それを押したらCORS未対応と対応済みAPIを呼ぶようにする。
その時にDev Toolsを見て、未対応の方はCORSのエラーが出ていて、対応済みの方はちゃんとOPTIONSメソッドとPOSTメソッドでリクエストが飛んでるか確認する。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>
<body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<button id="ng_cors">CORS無許可確認ボタン</button>
<button id="ok_cors">CORS許可確認ボタン</button>
<script type="text/javascript">
$(function () {
// CORS無許可確認ボタンクリック時
$("#ng_cors").on("click", function () {
$.ajax({
type: "POST",
url: "http://localhost:3000/api/v1/ng_cors",
contentType: "application/json",
})
// Ajaxリクエストが成功した時発動
.done((data) => {
console.log(data)
})
// Ajaxリクエストが失敗した時発動
.fail((data) => {
console.log(data)
})
// Ajaxリクエストが成功・失敗どちらでも発動
.always((data) => {})
})
// CORS許可確認ボタンクリック時
$("#ok_cors").on("click", function () {
$.ajax({
type: "POST",
url: "http://localhost:3000/api/v1/ok_cors",
contentType: "application/json",
})
// Ajaxリクエストが成功した時発動
.done((data) => {
console.log(data)
})
// Ajaxリクエストが失敗した時発動
.fail((data) => {
console.log(data)
})
// Ajaxリクエストが成功・失敗どちらでも発動
.always((data) => {})
})
})
</script>
</body>
</html>
できたはず。
こっちは、application/jsonでリクエストする以外、前回と変わったところはないかな。
【結果確認】実際にCORSでクロスドメインアクセス
では確認すべく準備する。
1. ローカルサーバー2つを別ポートで起動する
サーバー側:node app.jsで起動する。8080ポートになってるはず。
クライアント側:htmlを置いてるディレクトリで、python3 -m http.server 8000で起動する。8000ポートになってるはず。
2. http://localhost:8000 にブラウザでアクセスする
3. ブラウザでDeveloper Tool開く
前はnode app/app.jsで起動するって書いてたけど、あれはなぜかappというディレクトリの下にapp.jsを置いてたのでそうしてただけです。紛らわしかったかも。。
じゃあ早速、運命のボタンおす。
まず、「1. CORS許可してないAPIをContent-Typeをapplication/jsonでPOSTリクエストする」の確認。
期待結果は、CORSとかプリフライトリクエストに関するエラーが出て失敗する。
「Access to XMLHttpRequest at 'http://localhost:3000/api/v1/ng_cors' from origin 'http://localhost:8000' 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.」
だと。想定通りかな。
前のエラーと違うのは、太字の部分。シンプルリクエストではなくて、プリフライトリクエストだってことをブラウザがちゃんと判断してくれてる証拠かな。多分。
次、「2. CORS許可しているAPIをContent-Typeをapplication/jsonでPOSTリクエストする」の確認。
期待結果は、プリフライトリクエストが送られるはず(つまりOPTIONSメソッドとPOSTメソッドのリクエストが飛ぶ)
よし。ちゃんと「CORS対応頑張ったよ」が返ってきた。
頑張ったんですよ。私にしては。
こっちがOPTIONSメソッドのリクエスト。サーバ側で設定した204が返ってきてます。
次が本命のPOSTリクエスト。
最後に
心残りだった実験ができたので、満足。
プリフライトリクエストってなんか長いけど、なんかかっこいいと思わなくもない。😃
次は何を書こうかしら。