LoginSignup
12

More than 3 years have passed since last update.

AWSを利用した会員サイトをサーバーレスで実装しました

Last updated at Posted at 2020-07-28

AWSを利用した会員サイトをサーバーレスで実装しました。
始めはCognitoとS3でなんとかなるだろうと思っていましたが、
いざやってみるとどうにも思い通りにならず、四苦八苦しました。

調べてみると、どうやらCognitoとS3だけでは、S3に対してユーザー単位のコンテンツにしかアクセスできず、ユーザー共有のコンテンツにはアクセスできないみたいです。(私の調査不足/理解不足かもしれませんが...)

というわけで、以下のAWSの機能を利用して実装してみました。

  • S3
  • CloudFront
  • Cognito
  • API Gateway
  • Lambda

処理手順

  1. S3に公開フォルダと会員限定フォルダを作成する。
  2. S3に対してはCloudFrontを通してアクセスする。
  3. S3の会員限定フォルダへは、CloudFrontの閲覧者のアクセスを制限する機能のCookieを使用する。
  4. Cognitoで認証が成功した場合、認証情報のJWTトークンをAPI Gatewayのオーソライザーで検証する。
  5. API Gatewayのオーソライザーで検証が成功した場合、同APIをトリガーに実行されるLambda関数から、Cookieを取得する。
  6. 取得したCookieを使用して、会員限定ページへ遷移する。
  7. サインアウトするとき、Cookieを削除する。

つまり、会員ページへのアクセス制限にCloudFrontを使用し、
CognitoはCloudFrontで必要なCookieを取得するための認証ということになります。

1. S3バケットを作成する

一般公開するpublicフォルダと、ログインしたユーザーがアクセス可能なprivateフォルダを作成します。

\
├── private
│   ├── private-index.bundle.js
│   ├── private-index.bundle.js.map
│   └── private-index.html
└── public
    ├── public-index.bundle.js
    ├── public-index.bundle.js.map
    └── public-index.html

Static website hostingや、バケットポリシーを設定する必要はありません。

2. CroudFrontを設定する

手順1で作成したバケットにCloudFrontを介してアクセスするようにします。
privateフォルダにCookieを使用してアクセスするBehaviorを追加します。
これでprivateフォルダは、Cookieを指定しないとアクセスできないフォルダになります。

3. Cognitoを作成する

ユーザープールを作成する

作成について特筆すべき点はありませんが、以下の情報を控えておきます。

  • ユーザープールID
  • アプリクライアントID

IDプールを作成する

以下の点を留意してください。

  • 認証プロバイダーには、ユーザープールのユーザープールIDアプリクライアントIDを指定します。
  • IDプールのIDを控えておきます。

4. CloudFrontへアクセスするためのCookieを作成する

CloudFrontのキーペアを作成する

AWSアカウントにrootでログインし、CloudFront のキーペアを作成します。

  1. AWSアカウントにrootログインします。
  2. ルートアクセスキーの削除を選択します。
  3. セキュリティ認証情報の管理を選択します。
  4. CloudFront のキーペアを選択します。
  5. 新しいキーペアの作成を選択し、プライベートキーファイル(pk-XXXXXXXX.pem)とパブリックキーファイル(rsa-XXXXXXXX.pem)をダウンロードします。
    • 画面に表示されるアクセスキーIDを控えておいてください。

CloudFrontへアクセスするためのCookieを生成する

CloudFrontへアクセスするためには、3つのCookieが必要です。

  • key-pair
  • policy
  • signature
key-pair

CloudFront のキーペアアクセスキーIDです。

policy

Jsonから改行とインデントを取り除いたものになります。

  • ResourceはアクセスするURLを設定します。(CloudFrontのURL)
  • 最低限DateLessThan(URLの有効期限切れ日時)を設定しておく必要があります。
{
   "Statement":[
      {
         "Resource":"https://xxxxxxxx.cloudfront.net/*",
         "Condition":{
            "DateLessThan":{
               "AWS:EpochTime":2595364179
            }
         }
      }
   ]
}

インデントと改行を取り除いたものをファイルとして保存します。(ここではpolicy.jsonとします)

{"Statement":[{"Resource":"https://xxxxxxxx.cloudfront.net/*","Condition":{"DateLessThan":{"AWS:EpochTime":2595364179}}}]}

以下のコマンドの出力結果がpolicyの値となります。

$ cat ./policy.json | openssl base64 | tr '+=/' '-_~'
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
signature

以下のコマンドの出力結果がsignatureの値となります。

$ cat policy.json | openssl sha1 -sign <プライベートキーファイル> | openssl base64 | tr '+=/' '-_~'
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXX

5. Lambda関数を作成する

CloudFrontへアクセスするためのCookieの値を返す関数を作成します。

  • 手順4で作成した文字列を設定します。
  • CORSに対応したレスポンスになるようにヘッダーを設定します。
exports.handler = async () => {
  const keyPair = 'XXXXXXXX';
  const policy = 'XXXXXXXX';
  const signature = 'XXXXXXXX';

  const json = {
    policy: policy,
    signature: signature,
    keyPair: keyPair
  };

  const response = {
    "statusCode": 200,
    "headers": {
        "Content-Type": 'application/json',
        "Access-Control-Allow-Headers": 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
        "Access-Control-Allow-Methods": "GET",
        "Access-Control-Allow-Origin": "*"
    },
    "body": JSON.stringify(json)
  };

  return response;
};

6. API Gatewayを作成する

API GatewayでCognitoで作成したユーザーのJWT Tokenを検証するためにオーソライザーを作成します。

API Gatewayのオーソライザーを作成する

  1. オーソライザーの名前を入力します。
  2. タイプでは、Cognitoを選択します。
  3. Cognitoユーザープールでは、今回使用する作成済みのユーザープール指定します。
  4. トークンのソースには、Authorizationと入力します。 保存したら、テストしてみましょう。 認証トークンに、認証できたユーザーの情報から取得できるdata.signInUserSession.idToken.jwtTokenを入力してテストします。 ユーザー情報が出力されたらテストは成功です。

API Gatewayを作成する

  • GETメソッドの認可に先ほど作成したオーソライザーを指定します。
  • CROSを有効化します。
  • 手順5で作成したLambda関数と紐付けます。

おまけ

クライアント側のソースコードです。

cognitoの情報を記述したaws-export.js

const AwsConfig = {
  Auth: {
    // IDプールのID
    identityPoolId: 'us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
    // リージョン
    region: 'us-east-1',
    // プールID
    userPoolId: 'us-east-1_xxxxxxxx',
    // アプリクライアントID
    userPoolWebClientId: 'xxxxxxxxxxxxxxxxxxxxxxxxx',
    mandatorySignIn: true,
  },
  Storage: {
    // 使用するS3バケット名
    bucket: 'xxxxxxxx',
    // リージョン
    region: 'us-east-1'
  }
}

export default AwsConfig;

public-index.js

import Amplify, { Auth } from 'aws-amplify';
import AwsConfig from './aws-exports.js';
Amplify.configure(AwsConfig);

const getEmail = () => 
  document.getElementById('email').value;

const getPassword = () => 
  document.getElementById('password').value;

const signUp = () => {
  const email = getEmail();
  const password = getPassword();

  Auth.signUp(email, password)
    .then(data => console.log(data))
    .catch(err => console.log(err));
}

const signIn = () => {
  const email = getEmail();
  const password = getPassword();

  Auth.signIn(email, password)
    .then(data => {
      const url = 'https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev';
      const token = data.signInUserSession.idToken.jwtToken;

      fetch(url, {
        method: 'GET',
        headers: {
          'Authorization': token
        }
      })
      .then(res => res.json())
      .then(json => {
        document.cookie = `CloudFront-Key-Pair-Id=${json.keyPair}; path=/;`;
        document.cookie = `CloudFront-Policy=${json.policy}; path=/;`;
        document.cookie = `CloudFront-Signature=${json.signature}; path=/;`;
        document.cookie = `Email=${email}; path=/;`;
        location.href = '../private/private-index.html';
      })
      .catch(err => console.log(err));

    })
    .catch(err => console.log(err));
}

window.addEventListener('DOMContentLoaded', () => {
  const signUpButton = document.getElementById("signUpButton");
  signUpButton.addEventListener("click", () => {
    signUp();
  });

  const signInButton = document.getElementById("signInButton");
  signInButton.addEventListener("click", () => {
    signIn();
  });
});

private-index.js

import Amplify, { Auth } from 'aws-amplify';
import AwsConfig from './aws-exports.js';
Amplify.configure(AwsConfig);

const signOut = () => {
  console.log('Sign out Start.')

  // Cookieを削除します。
  document.cookie = "CloudFront-Key-Pair-Id=; path=/; max-age=0";
  document.cookie = "CloudFront-Policy=; path=/; max-age=0";
  document.cookie = "CloudFront-Signature=; path=/; max-age=0";
  document.cookie = "Email=; path=/; max-age=0";

  const url = '../public/public-index.html';

  Auth.signOut()
    .then(data => {
      console.log(data);
      location.href = url;
    })
    .catch(err => {
      console.log(err)
      location.href = url;
    });

}

window.addEventListener('DOMContentLoaded', () => {
  const signOutButton = document.getElementById("signOutButton");
  signOutButton.addEventListener("click", () => {
    signOut();
  });

  Auth.currentUserInfo()
    .then(user => console.log(user))
    .catch(err => console.log(err))
});

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
12