0
Help us understand the problem. What are the problem?

posted at

updated at

Lambda関数でOPTIONメソッドを作り「CORSの複数origin対応」を攻略する③(実装説明)

この問題にぶつかるまでの経緯

  • 単純でないリクエストでPreflightリクエストが送られるが、そこで特定の複数originからのリクエストのみパスさせたい
    • 本番環境では特に Access-Control-Allow-Origin: "*" はだめなので...
    • しかし、Access-Control-Allow-Origin: "https:hoge.com,https:huga.com" のような形式で複数のorigin を指定することはApiGatewayの仕様上できないらしい
      • ではどうするか?次のように考えた
        • PreflightリクエストはOPTIONS
        • OPTIONSメソッドを自分で作ったLambda関数に統合して、定義したホワイトリストに合致するoriginからのリクエストだった場合パスさせるようにする

記事の構成

対応方法まとめ

  • 複数originのCORS許可対応をさせたいメソッドについて
    • (下記手順内の例ではExampleFunctionというGETメソッドを指す)
    • レスポンスヘッダーの Access-Control-Allow-Origin は「*」とし全てのoriginをパスさせる
  • ExampleFunctionと同一エンドポイントのOPTIONSメソッドを定義
    • レスポンスヘッダーとして、リクエスト元のフロント側originを Access-Control-Allow-Origin にセットする処理とする

環境

  • macOS: Big Sur v11.6.1(Intel)
  • バックエンド(Lambda関数を呼び出すAPIをSAMで構築する)
    • SAM CLI: 1.42.0
      • Runtime: node.js 14.x (Typescript)
  • フロントエンド
    • 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!',
        }),
      };
    };

デプロイ

Untitled.png

  • 無事、Lambda関数に統合された

iOSアプリから呼び出してみる

  • 今回はホワイトリストとして下記をsamconfig.tomlで設定しています

エミュレータから

  • NGパターン(ポート番号が不一致)
    • Preflightでエラーとなった!

Untitled 1.png

Untitled 2.png

  • OKパターン
    • 無事APIが通った!

Untitled 3.png

Untitled 4.png

実機から

  • 実機はhost nameを変更しなければ ionic://localhost でoriginは固定
    • 無事APIが通った!

Untitled 5.png

結果まとめ

  • 下記の通り、想定通りの挙動を実現できた
    • ホワイトリストに登録された2つのoriginがPreflightをパス
    • 未登録のoriginが弾かれた

(参考)エラーの場合の原因調査方法

  • CloudWatch

    • Preflightが走っていればログが出力されているはず
      • ログが出力されてないならPreflightが走ってないかも?
      • 単純リクエストになってたりする可能性あり
  • Lambda関数のテスト

Untitled 6.png

  • 開発者ツールのネットワークタブ

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
0
Help us understand the problem. What are the problem?