AWSを利用した会員サイトをサーバーレスで実装しました
AWSを利用した会員サイトをサーバーレスで実装しました。
始めはCognitoとS3でなんとかなるだろうと思っていましたが、
いざやってみるとどうにも思い通りにならず、四苦八苦しました。
調べてみると、どうやらCognitoとS3だけでは、S3に対してユーザー単位のコンテンツにしかアクセスできず、ユーザー共有のコンテンツにはアクセスできないみたいです。(私の調査不足/理解不足かもしれませんが...)
というわけで、以下のAWSの機能を利用して実装してみました。
- S3
- CloudFront
- Cognito
- API Gateway
- Lambda
処理手順
- S3に公開フォルダと会員限定フォルダを作成する。
- S3に対してはCloudFrontを通してアクセスする。
- S3の会員限定フォルダへは、CloudFrontの閲覧者のアクセスを制限する機能のCookieを使用する。
- Cognitoで認証が成功した場合、認証情報のJWTトークンをAPI Gatewayのオーソライザーで検証する。
- API Gatewayのオーソライザーで検証が成功した場合、同APIをトリガーに実行されるLambda関数から、Cookieを取得する。
- 取得したCookieを使用して、会員限定ページへ遷移する。
- サインアウトするとき、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 のキーペアを作成します。
- AWSアカウントにrootログインします。
- ルートアクセスキーの削除を選択します。
- セキュリティ認証情報の管理を選択します。
- CloudFront のキーペアを選択します。
- 新しいキーペアの作成を選択し、プライベートキーファイル(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のオーソライザーを作成する
- オーソライザーの名前を入力します。
- タイプでは、Cognitoを選択します。
- Cognitoユーザープールでは、今回使用する作成済みのユーザープール指定します。
- トークンのソースには、
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))
});
Discussion