この記事は、「AWS Advent Calendar 2018」の16日目の記事です。
CognitoとAmplifyでフロントエンドのみでログイン機能を構築してみました!
ログイン機能は、通常バックエンドのプログラムやDBを利用しないと構築できないのですが、Amazon Cognitoを利用するとサーバーレスで実装可能になります。また、Amplifyを利用することによりAmazon Cognitoをより手軽に実装することが可能です。
全体の構成はできるだけシンプルに。
バックエンド - ユーザー管理
- Amazon Cognito
フロントエンド - ログイン機能
- Amplify
- Riot.js
- Materialize
- webpack
バックエンド
まずは、バックエンドを構築していきます。
今回は、ユーザー管理をするためにAmazon Cognitoを利用します。
初めにAWSのコンソールで[Cognito]と入力し、[Amazon Cognito]をクリック
[ユーザープールの管理]をクリック
[ユーザープールを作成する]をクリック
今回はプール名を[Login_Sample]を入力し、[ステップに従って設定する]をクリック
[ユーザー名]を選択、[email]にチェック、[次のステップ]をクリック
パスワードの任意条件を今回は画像の内容とし、[次のステップ]をクリック
MFA[オフ]を選択、検証要求[Eメール]をチェック、ロール名は自動で入るのでそのままとし、[次のステップ]をクリック
検証タイプ[コード]を選択、件名とメッセージは任意で[次のステップ]をクリック
タグは設定不要とし、[次のステップ]をクリック
ユーザーのデバイスの記憶[いいえ]を選択し、[次のステップ]をクリック
[アプリクライアントの追加]をクリック
アプリクライアント名は、今回[Login_sample]を入力、トークンの有効期限[30]のままとし、チェックボックスは全て外し、[アプリクライアントの作成]をクリック
[次のステップ]をクリック
設定はそのまま変更なしで[次のステップ]をクリック
[プールの作成]をクリック
ユーザープールが作成されたら、[プールID]をコピーしておきます。
[アプリクライアント]をクリック。[アプリクライアントID]をコピーしておきます。
次にIDプールを作成するため、[フェデレーティッドアイデンティティ]をクリック
[新しいIDプールの作成]をクリック
IDプール名を今回は[Login_Sample]を入力、[認定されていないIDに対してアクセスを有効にする]をチェック、ユーザープールIDに[プールID]を入力、アプリクライアントIDに[アプリクライアントID]を入力、[プールの作成]をクリック
ロールの設定はそままで、[許可]をクリック
IDプールが作成されたら、[IDプールのID]をコピーしておきます。
ここまでで、バックエンドのユーザー管理の設定は完了です。
フロントエンド
次に、フロントエンドを構築していきます。
Amplifyでログイン機能を実装する場合は、React・Angular・Vue.jsは既にサンプルが存在するのでそのままサンプルを利用いただければと思います。
今回は、riot-starterというRiot.jsを手軽に始めるビルド環境を利用してRiot.jsで構築してみます。
ログイン機能を構築
全体構成
package.json
{
"name": "login_sample",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server"
},
"keywords": [],
"author": "Yasunori Kirimoto",
"license": "ISC",
"devDependencies": {
"riot": "^3.13.1",
"riot-hot-reload": "^1.0.0",
"riot-tag-loader": "^2.1.0",
"webpack": "^4.20.2",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.9"
},
"dependencies": {
"aws-amplify": "^0.4.8",
"aws-sdk": "^2.377.0",
"css-loader": "^1.0.0",
"file-loader": "^2.0.0",
"jquery": "^3.3.1",
"materialize-css": "^1.0.0",
"ress": "^1.2.2",
"riot-route": "^3.1.4",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2"
}
}
webpack.config.js
const webpack = require('webpack');
module.exports = {
entry: './_resouce/main.js',
output: {
path: __dirname + '/dist',
filename: 'app.js'
},
module : {
rules : [
{
test: /\.tag$/,
exclude: /node_modules/,
loader: 'riot-tag-loader',
query: {
hot: true,
debug: true
}
},
{
test: /\.css$/,
loaders: ['style-loader', 'css-loader']
},
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader:"url-loader?limit=10000&mimetype=application/font-woff"
},
{
test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: "file"
}
]
},
plugins: [
new webpack.ProvidePlugin({
riot: 'riot',
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery'
})
],
devServer: {
contentBase: __dirname + '/dist',
publicPath: '/',
watchContentBase: true,
open: true
}
};
main.js
// JS一式を読み込んでパッケージ
import {amplify_conf} from './js/amplify_conf.js';
// CSS一式を読み込んでパッケージ
import 'ress/ress.css';
import 'materialize-css/dist/css/materialize.css';
import './css/icon.css';
import './css/style.css';
// materialize(JS)を読み込んでパッケージ
import 'materialize-css';
// riot-route読み込み
import route from 'riot-route';
// サインアップページ読み込んでパッケージ
import './tag/signup_page.tag';
route('/signup', function() {
riot.mount('app','signup_page', {
type: 'signup',
amplify_conf: amplify_conf
});
});
// サインインページ読み込んでパッケージ
import './tag/signin_page.tag';
route('/', function() {
riot.mount('app','signin_page', {
type: 'signin',
amplify_conf: amplify_conf
});
});
// トップページ読み込んでパッケージ
import './tag/top_page.tag';
route('/top', function() {
riot.mount('app','top_page', {
type: 'top',
amplify_conf: amplify_conf
});
});
// ルータースタート
route.start(true);
amplify_conf.js
// Amplify読み込み
import Amplify from 'aws-amplify';
export function amplify_conf() {
// キーを設定
Amplify.configure({
Auth: {
// IDプールのID
identityPoolId: 'xxxxx',
// リージョン名(基本固定)
region: 'ap-northeast-1',
// ユーザープールのプールID
userPoolId: 'ap-northeast-1_xxxxx',
// ユーザープールのアプリクライアントID
userPoolWebClientId: 'xxxxx'
}
});
}
signin_page.tag
<signin_page>
<div id="m_top_120"></div>
<!--題名-->
<div class="row">
<div class="container">
<div class="col s12 center">
<h4>
サインイン
</h4>
</div>
</div>
</div>
<div id="m_top_40"></div>
<!--ユーザー名&パスワード入力-->
<div class="row">
<div class="container">
<div class="col s12">
<div class="col s4">
</div>
<div class="input-field col s4">
<input type="text" class="validate" ref="usernameValue" required>
<label>ユーザー名</label>
</div>
<div class="col s4">
</div>
</div>
<div class="col s12">
<div class="col s4">
</div>
<div class="input-field col s4">
<input type="password" class="validate" ref="passwordValue" required>
<label>パスワード</label>
</div>
<div class="col s4">
</div>
</div>
</div>
</div>
<div id="m_top_40"></div>
<!--サインインボタン-->
<div class="row">
<div class="container">
<div class="col s12 center">
<button class="btn-large waves-light light-blue darken-4" type="button" name="action" onclick={click_signin}>
サインイン
</button>
</div>
</div>
</div>
<div id="m_top_80"></div>
<!--サインアップへ移動ボタン-->
<div class="row">
<div class="container">
<div class="col s4">
</div>
<div class="col s3">
<button class="btn waves-effect waves-light deep-orange lighten-1" type="button" name="action" onclick={click_signup_go}>
サインアップへ
</button>
</div>
<div class="col s5">
</div>
</div>
</div>
<style scoped>
/*各余白*/
#m_top_40{
margin-top: 40px;
}
#m_top_80 {
margin-top: 80px;
}
#m_top_120 {
margin-top: 120px;
}
</style>
<script>
// ページ読み込み時処理
this.on('mount', function() {
// 関数読み込み
var amplify_conf = opts.amplify_conf;
// ユーザー認証設定
amplify_conf();
// AmplifyのAuth読み込み
import { Auth } from 'aws-amplify';
// セッション処理
Auth.currentSession()
.then(function() {
$('body').css('display','none');
console.log ('ログイン済');
var URL = window.location.pathname;
location.href = URL + "#top";
})
.catch(function() {
console.log('ログイン未');
});
});
// サインインボタンクリック時処理
this.click_signin = function() {
// ユーザー名・パスワード取得
var v_username = this.refs.usernameValue.value;
var v_password = this.refs.passwordValue.value;
// サインイン処理
Auth.signIn(v_username, v_password)
.then(function() {
console.log ('サインインに成功しました');
var URL = window.location.pathname;
location.href = URL + "#top";
})
.catch(function() {
console.log('サインインに失敗しました');
});
}.bind(this);
// サインアップへボタンクリック時処理
this.click_signup_go = function() {
console.log("サインアップに移動");
var URL = window.location.pathname;
location.href = URL + "#signup";
}.bind(this);
</script>
</signin_page>
signup_page.tag
<signup_page>
<div id="m_top_120"></div>
<!--題名-->
<div class="row">
<div class="container">
<div class="col s12 center">
<h4>
サインアップ
</h4>
</div>
</div>
</div>
<div id="m_top_40"></div>
<!--ユーザー名&パスワード&メールアドレス入力-->
<div class="row">
<div class="container">
<div class="col s12">
<div class="col s4">
</div>
<div class="input-field col s4">
<input type="text" class="validate" ref="usernameValue" required>
<label>ユーザー名</label>
</div>
<div class="col s4">
</div>
</div>
<div class="col s12">
<div class="col s4">
</div>
<div class="input-field col s4">
<input type="password" class="validate" ref="passwordValue" required>
<label>パスワード</label>
</div>
<div class="col s4">
</div>
</div>
<div class="col s12">
<div class="col s4">
</div>
<div class="input-field col s4">
<input type="text" class="validate" ref="mailValue" required>
<label>メールアドレス</label>
</div>
<div class="col s4">
</div>
</div>
</div>
</div>
<div id="m_top_40"></div>
<!--サインアップボタン-->
<div class="row">
<div class="container">
<div class="col s12 center">
<button class="btn-large waves-light light-blue darken-4" type="button" name="action" onclick={click_signup}>
サインアップ
</button>
</div>
</div>
</div>
<div id="u_code">
<div id="m_top_40"></div>
<!--認証コード入力-->
<div class="row" id="u_code">
<div class="container">
<div class="col s12">
<div class="col s4">
</div>
<div class="input-field col s4">
<input type="text" class="validate" ref="authValue" required>
<label>認証コード</label>
</div>
<div class="col s4">
</div>
</div>
</div>
</div>
<div id="m_top_40"></div>
<div id="m_top_60"></div>
<!--承認ボタン-->
<div class="row">
<div class="container">
<div class="col s12 center">
<button class="btn-large waves-light light-blue darken-4" type="button" name="action" onclick={click_auth}>
承認
</button>
</div>
</div>
</div>
</div>
<style scoped>
/*各余白*/
#m_top_40{
margin-top: 40px;
}
#m_top_120 {
margin-top: 120px;
}
</style>
<script>
// ページ読み込み時処理
this.on('mount', function() {
// 関数読み込み
var amplify_conf = opts.amplify_conf;
// ユーザー認証設定
amplify_conf();
// AmplifyのAuth読み込み
import { Auth } from 'aws-amplify';
// セッション処理
Auth.currentSession()
.then(function() {
$('body').css('display','none');
console.log ('ログイン済');
var URL = window.location.pathname;
location.href = URL + "#top";
})
.catch(function() {
console.log('ログイン未');
});
//認証コード入力項目非表示
$('#u_code').css('display','none');
});
// サインアップボタンクリック時処理
this.click_signup = function() {
// ユーザー名・パスワード取得
var v_username = this.refs.usernameValue.value;
var v_password = this.refs.passwordValue.value;
var v_mail = this.refs.mailValue.value;
// サインイン処理
Auth.signUp(v_username, v_password, v_mail)
.then(function() {
console.log ('サインインに成功しました');
//認証コード入力項目表示
$('#u_code').css('display','block');
})
.catch(function() {
console.log('サインインに失敗しました');
});
}.bind(this);
// 承認ボタンクリック時処理
this.click_auth = function() {
// ユーザー名、認証コード
var v_username = this.refs.usernameValue.value;
var v_code = this.refs.authValue.value;
// サインイン処理
Auth.confirmSignUp(v_username, v_code)
.then(function() {
console.log ('認証に成功しました');
var URL = window.location.pathname;
location.href = URL + "#top";
})
.catch(function() {
console.log('認証に失敗しました');
});
}.bind(this);
</script>
</signup_page>
top_page.tag
<top_page>
<div id="m_top_40"></div>
<!--サインアウトボタン-->
<div class="row">
<div class="container">
<div class="col s9">
</div>
<div class="col s3">
<button class="btn waves-effect waves-light deep-orange lighten-1" type="button" name="action" onclick={click_sign_out}>
サインアウト
</button>
</div>
</div>
</div>
<div id="m_top_100"></div>
<!--題名-->
<div class="row">
<div class="container">
<div class="col s12 center">
<h4>
サインインしています!
</h4>
</div>
</div>
</div>
<style scoped>
/*各余白*/
#m_top_40{
margin-top: 40px;
}
#m_top_100 {
margin-top: 100px;
}
</style>
<script>
// ページ読み込み時処理
this.on('mount', function() {
// 関数読み込み
var amplify_conf = opts.amplify_conf;
// ユーザー認証設定
amplify_conf();
// AmplifyのAuth読み込み
import { Auth } from 'aws-amplify';
// セッション処理
Auth.currentSession()
.then(function() {
$('body').css('display','block');
console.log ('成功しました');
})
.catch(function() {
$('body').css('display','none');
console.log('失敗しました');
var URL = window.location.pathname;
location.href = URL;
});
});
// サインアウトボタンクリック時処理
this.click_sign_out = function() {
Auth.signOut()
.then(function() {
console.log ('サインアウト成功しました');
var URL = window.location.pathname;
location.href = URL;
})
.catch(function() {
console.log('サインアウト失敗しました');
});
}.bind(this);
</script>
</top_page>
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>login_sample</title>
</head>
<body>
<app></app>
<script src="./app.js"></script>
</body>
</html>
実行環境
node v10.0.0
npm v6.4.1
パッケージインストール
npm install
ビルド
npm run build
開発用
npm run dev
サインアップ画面が起動されたら、ユーザー登録してみます。
サインアップボタン実行後、Amazon Cognitoの画面を確認してみます。[ユーザーとグループ]をクリックし、ユーザーが登録されているのが確認できます。ただ、このままではステータスが未認証のままになっています。
指定したメールアドレスに認証コードが送られてくるので、認証コードをコピーし、サインアップ画面で認証コードを入力し実行します。
再度、Amazon Cognitoの画面で[ユーザーとグループ]をクリックし、ステータスが認証済になっているを確認できます。これでサインアップ完了です。
最後に、さきほど作成したユーザーでサインインしてみます。
CognitoとAmplifyでフロントエンドのみでログイン機能を構築できることが確認できました!
今回は機能を絞った構築にしましたが、AmplifyはAPI Gateway等も連携できるのでAWSの様々なサーバーレスサービスで色々とできそうですね!