この問題にぶつかるまでの経緯
- 単純でないリクエストでPreflightリクエストが送られるが、そこで特定の複数originからのリクエストのみパスさせたい
- 本番環境では特に
Access-Control-Allow-Origin: "*"
はだめなので... - しかし、
Access-Control-Allow-Origin: "https:hoge.com,https:huga.com"
のような形式で複数のorigin を指定することはApiGatewayの仕様上できないらしい- ではどうするか?次のように考えた
- PreflightリクエストはOPTIONS
- OPTIONSメソッドを自分で作ったLambda関数に統合して、定義したホワイトリストに合致するoriginからのリクエストだった場合パスさせるようにする
- ではどうするか?次のように考えた
- 本番環境では特に
記事の構成
-
[AWS SAM] Lambda関数でOPTIONメソッドを作り「CORSの複数origin対応」を攻略する①(概要説明)
- 全体的な説明、CORSについてなど
-
[AWS SAM] Lambda関数でOPTIONメソッドを作り「CORSの複数origin対応」を攻略する②(シンプルな代案の説明)
- originが「*」や特定の1originで良い場合の説明
- [AWS SAM] Lambda関数でOPTIONメソッドを作り「CORSの複数origin対応」を攻略する③(実装説明)
- 本記事
対応方法まとめ
- 複数originのCORS許可対応をさせたいメソッドについて
- (下記手順内の例ではExampleFunctionというGETメソッドを指す)
- レスポンスヘッダーの
Access-Control-Allow-Origin
は「*」とし全てのoriginをパスさせる
-
ExampleFunctionと同一エンドポイントのOPTIONSメソッドを定義
- レスポンスヘッダーとして、リクエスト元のフロント側originを
Access-Control-Allow-Origin
にセットする処理とする
- レスポンスヘッダーとして、リクエスト元のフロント側originを
環境
- macOS: Big Sur v11.6.1(Intel)
- バックエンド(Lambda関数を呼び出すAPIをSAMで構築する)
- SAM CLI: 1.42.0
- Runtime: node.js 14.x (Typescript)
- SAM CLI: 1.42.0
- フロントエンド
- Ionic: @ionic/angular 5.8.0
手順
普通のGET, POSTメソッドなどを作るのと同様にOPTIONSメソッドを作る
ローカルからLambdaに渡す環境変数としてoriginのホワイトリストを定義
- ここで定義されたものが許可するoriginとなる
- samconfig.tomlはGit管理されていないものとしている
samconfig.toml
[default.deploy.parameters]
parameter_overrides = "ORIGINWHITELIST=\"ionic://localhost,http://localhost:8100\""
OPTIONSを作成
template.yml
# samconfig.tomlで設定したパラメータを環境変数として設定する
Parameters:
++ ORIGINWHITELIST:
++ Type: String
Globals:
Function:
Environment:
++ Variables:
++ ORIGINWHITELIST: !Ref ORIGINWHITELIST
# OPTIONSを定義する
Resources:
ExampleFunction:
Type: AWS::Serverless::Function
Properties:
Handler: dist/handlers/example.exampleHandler # TypescriptをJavascriptにコンパイルしているのが/dist配下。Typescriptを直接実行はできないのでそこを参照させる。tsconfig.json: compilerOptions.outDirを参照。
Events:
Api:
Type: Api
Properties:
Path: /
Method: GET
RestApiId: !Ref ApiGateway
++ OptionFunction:
++ Type: AWS::Serverless::Function
++ Properties:
++ Handler: dist/handlers/preflight.preflightHandler # TypescriptをJavascriptにコンパイルしているのが/dist配下。Typescriptを直接実行はできないのでそこを参照させる。tsconfig.json: compilerOptions.outDirを参照。
++ Events:
++ Api:
++ Type: Api
++ Properties:
++ Path: /
++ Method: OPTIONS
++ RestApiId: !Ref ApiGateway
- レスポンスヘッダーとして、リクエスト元のフロント側originを
Access-Control-Allow-Origin
にセットする処理とする
src/handlers/preflight.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import 'source-map-support/register';
/**
* OPTION(preflight)用
*/
export const preflightHandler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
console.debug('event', event);
console.debug('process', process);
const originWhiteList = process.env.ORIGINWHITELIST;
const origin = event.headers['origin'];
const responseHeaders = {
'Access-Control-Allow-Origin': '',
'Access-Control-Allow-Credentials': true,
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization',
};
if (!originWhiteList) {
// Origin環境変数の取得失敗の場合
console.error('Cannot get env[ORIGINWHITELIST]');
console.error('event.headers: ', event.headers);
return {
headers: responseHeaders,
statusCode: 403,
body: JSON.stringify({
message: 'Missing white list of origin!',
}),
};
}
if (!origin) {
// リクエストヘッダーにoriginが含まれていない場合
console.error('headers.origin has not passed.');
console.error('event.headers: ', event.headers);
return {
headers: responseHeaders,
statusCode: 403,
body: JSON.stringify({
message: 'Missing passed origin!',
}),
};
}
if (!originWhiteList.split(',').includes(origin)) {
// ホワイトリストに渡されたoriginが含まれていない場合
console.error('Origin whitelist not include passed origin.');
console.error('originWhiteList.split(', '): ', originWhiteList.split(','));
console.error('origin: ', origin);
return {
headers: responseHeaders,
statusCode: 403,
body: JSON.stringify({
message: 'Not allowed origin!',
}),
};
}
// 全チェックをパスしたので、ここでフロント側originをAccess-Control-Allow-Originにセットしている
// これでAccess-Control-Allow-Originには1つのoriginしか記載できないという制限をクリアしつつ、複数origin(Lambda環境変数にて指定したoriginのホワイトリストに載っているorigin)に対応できた
responseHeaders['Access-Control-Allow-Origin'] = origin;
console.debug('Origin check is passed!');
console.debug('responseHeaders:', responseHeaders);
return {
headers: responseHeaders,
statusCode: 200,
body: JSON.stringify({
message: 'OK',
}),
};
};
CORS対応させたいメソッドの設定
- レスポンスヘッダーの
Access-Control-Allow-Origin
は「*」とし全てのoriginをパスさせる
example.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import 'source-map-support/register';
/**
* A simple example includes a HTTP get method.
*/
export const exampleHandler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
// All log statements are written to CloudWatch
console.debug('Received event:', event);
return {
statusCode: 200,
++ headers: {
++ 'Access-Control-Allow-Origin': '*',
++ 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
++ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, Accept, timeout',
++ },
body: JSON.stringify({
message: 'Hello world!',
}),
};
};
デプロイ
- 無事、Lambda関数に統合された
iOSアプリから呼び出してみる
- 今回はホワイトリストとして下記をsamconfig.tomlで設定しています
- http://localhost:8100 (エミュレータ用)
- ionic://localhost (実機用)
エミュレータから
- NGパターン(ポート番号が不一致)
- Preflightでエラーとなった!
- OKパターン
- 無事APIが通った!
実機から
- 実機はhost nameを変更しなければ
ionic://localhost
でoriginは固定- 無事APIが通った!
結果まとめ
- 下記の通り、想定通りの挙動を実現できた
- ホワイトリストに登録された2つのoriginがPreflightをパス
- 未登録のoriginが弾かれた
(参考)エラーの場合の原因調査方法
-
CloudWatch
- Preflightが走っていればログが出力されているはず
- ログが出力されてないならPreflightが走ってないかも?
- 単純リクエストになってたりする可能性あり
- Preflightが走っていればログが出力されているはず
-
Lambda関数のテスト
- 開発者ツールのネットワークタブ