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

posted at

updated at

[AWS SAM] Lambda関数でOPTIONメソッドを作り「CORSの複数origin対応」を攻略する①(概要説明)

この記事で解説することを3stepで

  • ① CORS許可設定で複数originを許可するOPTIONSメソッドをLambda関数で定義する。
  • ② その際にAWSコンソールを弄ってしまったらせっかくSAMで構築している意味が激減する(?)と思っているので絶対に弄らない。
    • いちいちPreflight用のOPTIONメソッドをコンソールからいじるのは面倒だし、SAMと後々競合とかしそうで怖い
    • (確認はAWSコンソールを見るのが手っ取り早いので見ます)
  • ③ その他CORSに関する説明も交える

記事の構成

結論を見たいだけな場合は③を参照してください!

環境

  • 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

前提

今回やろうとしていること

  • IonicでiOS・Android両対応のアプリを作ってAPIを呼ぶ
    • 今回はiOSでの動作確認方法で記載しています
  • APIから呼ばれるLambda関数は仮想通貨取引所のAPIをcallして情報を取得し、フロント側にレスポンスする

Untitled.png

(上記図はPlantUMLで作図してます)

SAMでAPIの構築が終わっている

  • ここではSAMでAPI Gatewayを構築する手順などは省き、CORSに関連する情報を記載します

攻略開始

まずはCORSについて

(自分の理解で説明するので、間違ってたら突っ込んでほしいです!)

  • APIを呼び出す際、フロントからの呼び出しはCORSによる制限がかかる

CORSのチェックフロー

  • 簡単には、下記フローになる
    • HTMLのiframeタグでは云々...とか他条件もありますが省いてます

Untitled 1.png

(上記フローチャートはmermaid.jsで作図してます)

Preflight リクエストのざっくり解説

  • 要するに、「リクエストが来たけど、サーバー側がちゃんと許可したリクエストか?」ということをチェックするため、本来送りたかったメソッドの前にOPTIOSメソッドを走らせて確認すること

バック(API側)でCORS対応を何もしていない状態について

バック(API側)

  • 下記の感じで、「/」に「GET」メソッドが存在する状況だとします
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import 'source-map-support/register';

export const exampleHandler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
	// ポイント: return オブジェクトに「headers」を含んでいない
  return {
    statusCode: 200, 
    body: JSON.stringify({
      message: 'Hello world!',
    }),
  };
};
AWSTemplateFormatVersion: 2010-09-09
Description: >-
  QryptoAle_API_SAM_Typescript2

Transform: 'AWS::Serverless-2016-10-31'

Parameters:
  ENV:
    Type: String
    Default: dev

Resources:
  # -------------------ApiGateway
  ApiGateway:
    Type: AWS::Serverless::Api
    Properties:
      Name: !Sub ${ENV}-${APINAME}
      StageName: !Ref ENV
  # -------------------Lambda
  ExampleFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: dist/handlers/example.exampleHandler # TypescriptをJavascriptにコンパイルしているのが/dist配下
      Events:
        Api:
          Type: Api
          Properties:
            Path: /
            Method: GET
            RestApiId: !Ref ApiGateway

フロント側

  • フレームワークはAngularを使ってます
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { PortfolioProfitRatio } from 'src/app/shared/interface/binance';
import { ApiService } from '../../shared/services/api.service';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {
  portfolioProfitRatios: PortfolioProfitRatio[];

  /** コンストラクタ */
  constructor(private api: ApiService) {}

  /** 初期化 */
  ngOnInit() {
    this.fetchPortfolioProfitRatios();
  }

	/** API call */
  fetchPortfolioProfitRatios(): Observable<any> {
    const ob = new Observable<any>(observable => {
      this.api.get<any>(`/`).subscribe(
        response => {
          observable.next(response);
        },
        error => {
          observable.error(error);
        }
      );
    });
    return ob;
  }
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../../../environments/environment';

@Injectable({
  providedIn: 'root',
})
export class ApiService {
  /** コンストラクタ */
  constructor(private httpClient: HttpClient) {}

  /**
   * 汎用get関数
   *
   * @param endpoint
   * @returns
   */
  get<T>(endpoint: string): Observable<T> {
    const ob = new Observable<T>(observable => {
      this.httpClient.get<T>(environment.origin + endpoint).subscribe(
        response => {
          observable.next(response);
        },
        error => {
          observable.error(error);
        }
      );
    });
    return ob;
  }
}

api.service.tsの#get 内でAPIをcallしています。

このリクエストはフロントからの呼び出しですが単純リクエストとなりますのでPreflightは走りません。

しかし、バック側でCORSに関する設定を何もしていません。
(この場合、そもそもフロントからの呼び出しができません)

まずはバック側でシンプルなCORS設定をしてあげようと思います。

バック側でシンプルなCORS設定をする

[AWS SAM] シンプルなOPTIONメソッドを自動生成する - Qiita

上記記事を参照してください。

記事内にも書きましたが、Access-Control-Allow-Originは複数のoriginの記載に対応していません。(ようやく今回の記事の本題に移れる...)

ではどうするかというと、Preflightリクエストで呼ばれるOPTIONSメソッドを自分で定義して複数originに対応させることになります!

(この記事の本題)Preflight用のLambda関数を統合して複数originに対応させる

[AWS SAM] Preflight(OPTIONS)をLambda関数で自作する(複数originの許可対応) - Qiita

  • この記事の一番最初に「結論を先読みしたい人向け」として貼った記事と同じです
  • 文量が多くなっちゃったので別記事として切り出しました

終わりに

この記事ではCORSについて学び直すにあたり
・Preflight用のOPTIONメソッドを
・Lambda関数で定義して
・複数originに対応しつつ
・AWS SAMでデプロイ
したい!と思い至るまでの経緯やCORS解説などをなるべく詳細に書きました。

具体的な実装方法は別記事としてますので、そちらを見るだけで充分ですが
・どういうシチュエーションで
・どういう背景があって
という部分も解説をしたほうがよりわかりやすいかなーと思い書きました。

書いているうちに自分の知識の整理や曖昧な部分が見えてくるのでそれも狙いでした!

AWSコンソールでマッピングテンプレートを設定するなどの記事もありましたが、SAM使っているならやっぱ少し手間でもコードに落とし込みたい(手動の設定は挟みたくない)と思ったので色々調べました。
業務で少しCORS扱ったのでいけるだろと思ったんですが、思ったよりつまづきポイントがあり、理解がまだまだ甘かったことを突きつけられました...(記事ではスマートに、一直線にできたような雰囲気を醸し出してますがめちゃめちゃトライ&エラーしてます)。

この記事がお役に立てば幸いです!

参考

AWSで複数オリジンのホワイトリストチェックするAPIを実装する

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?