こんにちは、広野です。
SPA (Single Page Application) では、技術的な制約上、フロントエンドのアプリ内でユーザのソース IP アドレスを取得することができません。そのため、一旦インターネットに通信を送って取得する必要があります。
ソース IP アドレスを返してくれるサービスは世の中にあるのですが、安心して使用するために自分で Amazon API Gateway だけでつくってみました。
セキュリティ要件から、アプリ側でソース IP アドレスをもとに何かしらの制御が必要になった際に活用できると思います。
やりたいこと
イメージは以下の図の通りです。
実現方法
- Amazon API Gateway Rest API を使用する。HTTP API ではダメ。
- Amazon API Gateway には主にログ取得を目的とした変数をデフォルトで持っており、その中にソース IP アドレスの変数がある。これをユーザへのレスポンスとして返してあげる。
- Amazon API Gateway で持つ変数を返すだけなので、他のサービスとの統合は不要。統合タイプは MOCK にする。
- Amazon API Gateway はまだ IPv6 に対応していないようで、(公式なドキュメントは見つけられませんでした)取得できる IP アドレスは IPv4 のみとなる。
気を付けるところは赤枠の2箇所。
統合リクエストの追記はこんな感じ。↓
統合レスポンスの追記はこんな感じ。↓
これ以外に CORS の設定がありますが、手作業だとハマる箇所なので、実装は最後に紹介する AWS CloudFormation テンプレートで行った方が確実です。CORS 対応のため、上記で紹介した GET メソッド以外に OPTIONS メソッドの定義も必要になります。テンプレートには含まれています。
SPA との結合例
ここでは React アプリからこの API を呼び出すときの実装例を紹介します。
アプリ側で、以下のコードを書きます。(関係ないコードは省略しています)
import React, { useEffect, useState } from 'react'; import axios from 'axios'; //state定義 const [sourceIp, setSourceIp] = useState(); //ソースIP取得関数 const getSourceIp = async () => { const res = await axios.get("https://xxxxxxxxxxxxxxxxx.amazonaws.com/prod/GetSourceIp"); //API Gateway のエンドポイント setSourceIp(res.data.sourceIp); console.log(res.data); //データの確認用 }; //画面表示時にソースIP取得 useEffect(() => { getSourceIp(); }, []); //画面表示 return ( <p>sourceIp: {sourceIp}</p> );
実際の画面では、以下のように表示されました。開発者コンソールでも、console.log で出力した情報が表示されています。
今回は画面表示させましたが、取得したソース IP アドレスをもとに条件分岐させて、画面内の処理を変えることができるようになると思います。
AWS CloudFormation テンプレート
とりあえず動く Amazon API Gateway のテンプレートを貼り付けておきます。
CORS 対応のためドメイン名をパラメータで入力しますが、ワイルドカード (*) にしたい方は適宜修正してご利用ください。
若干冗長な設定があるのは認識しているのですが、そこまで検証しきれず、リリースいたします。
AWSTemplateFormatVersion: 2010-09-09 Description: CloudFormation template that creates a API Gateway and a relevant IAM roles for getting the source IP address. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SystemName: Type: String Description: System name. Default: xxxxx MaxLength: 10 MinLength: 1 DomainName: Type: String Description: Domain name for URL. xxxxx.xxx Default: xxxxx.xxx MaxLength: 40 MinLength: 5 AllowedPattern: "[^\\s@]+\\.[^\\s@]+" SubDomainName: Type: String Description: Sub domain name for URL. Default: example-xxxxx MaxLength: 20 MinLength: 1 Resources: # ------------------------------------------------------------# # API Gateway Role (IAM) # ------------------------------------------------------------# ApiGatewayRole: Type: AWS::IAM::Role Properties: RoleName: !Sub example-ApiGatewayRole-${SystemName} Description: This role allows API Gateways to execute. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess # ------------------------------------------------------------# # API Gateway # ------------------------------------------------------------# RestApiGetSourceIp: Type: AWS::ApiGateway::RestApi Properties: Name: !Sub example-RestApi-GetSourceIp-${SystemName} Description: !Sub example REST API Gateway to get the user's source IP address for example-${SystemName} EndpointConfiguration: Types: - REGIONAL Tags: - Key: Cost Value: !Sub example-${SystemName} RestApiDeploymentGetSourceIp: Type: AWS::ApiGateway::Deployment Properties: RestApiId: !Ref RestApiGetSourceIp DependsOn: - RestApiMethodGetSourceIpGet - RestApiMethodGetSourceIpOptions RestApiStageGetSourceIp: Type: AWS::ApiGateway::Stage Properties: StageName: prod Description: production stage RestApiId: !Ref RestApiGetSourceIp DeploymentId: !Ref RestApiDeploymentGetSourceIp TracingEnabled: false Tags: - Key: Cost Value: !Sub example-${SystemName} DependsOn: RestApiDeploymentGetSourceIp RestApiResourceGetSourceIp: Type: AWS::ApiGateway::Resource Properties: RestApiId: !Ref RestApiGetSourceIp ParentId: !GetAtt RestApiGetSourceIp.RootResourceId PathPart: GetSourceIp DependsOn: - RestApiGetSourceIp RestApiMethodGetSourceIpGet: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref RestApiGetSourceIp ResourceId: !Ref RestApiResourceGetSourceIp HttpMethod: GET AuthorizationType: NONE Integration: Type: MOCK Credentials: !GetAtt ApiGatewayRole.Arn IntegrationResponses: - StatusCode: 200 ResponseTemplates: application/json: '{"sourceIp": "$context.identity.sourceIp"}' ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'" method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" method.response.header.Access-Control-Allow-Origin: !Sub "'https://${SubDomainName}.${DomainName}'" PassthroughBehavior: WHEN_NO_MATCH RequestTemplates: application/json: '{"statusCode": 200}' MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Headers: true method.response.header.Access-Control-Allow-Methods: true method.response.header.Access-Control-Allow-Origin: true DependsOn: - RestApiResourceGetSourceIp - ApiGatewayRole RestApiMethodGetSourceIpOptions: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref RestApiGetSourceIp ResourceId: !Ref RestApiResourceGetSourceIp HttpMethod: OPTIONS AuthorizationType: NONE Integration: Type: MOCK Credentials: !GetAtt ApiGatewayRole.Arn IntegrationResponses: - ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'" method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" method.response.header.Access-Control-Allow-Origin: !Sub "'https://${SubDomainName}.${DomainName}'" ResponseTemplates: application/json: '' StatusCode: 200 PassthroughBehavior: WHEN_NO_MATCH RequestTemplates: application/json: '{"statusCode": 200}' MethodResponses: - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Headers: true method.response.header.Access-Control-Allow-Methods: true method.response.header.Access-Control-Allow-Origin: true StatusCode: 200 DependsOn: - RestApiResourceGetSourceIp - ApiGatewayRole
まとめ
いかがでしたでしょうか?
なにげに世の中にこのような情報が少なかったので、ブログ記事にしてみました。
本記事が皆様のお役に立てれば幸いです。