DEVELOPMENT
目次
最近ではちょっとしたwebアプリなら、サーバーサイドを使わずに、Node環境でReactやVueなどを使った開発が多くなってきました。 しかし、SPAではなく、ちゃんとしたショッピングサイト的なものを作ろうと思うと、どうしてもログイン機能などが必要になって来ると思います。
せっかくならクライアントサイドだけの開発でサーバーサイドはクラウドサービスにお任せしたくなってきましたので、ログインも便利なCognitoにおまかせして実装してみようということで、やってみました。
Amplifyは?
最近良く耳にしたり、記事を目にしたりすることが多いですが、個人的には、まだまだ怖いので、使いたくない印象です。というのも、勝手に作成されるIAMに、ルートのスーパーユーザーの権限を与えたり、見えないところで勝手に色々設定をされるので、ちょっと嫌だなーという印象を受けたので、まだ実サービスでは、使うのはやめておこうというところです。
なので、今回は、Cognitoの設定も自分でして、ログインや認証処理等も下記のライブラリを使って自前で実装しますよ。Vueになれるためにもちょうどいいかもしれません。
今回、cognitoの認証部分等、こちらを参考にさせていただきました。
▶ 参考 vue-cliで作成したSPAにシンプルにCognitoログインを組み込む
状況
- node v10.0.0(ndenv使用)
- vue アプリはvue cliで作成している
- ページ遷移はvue-routerを使っている
- 必要に応じて、store(vuex)を使う
使うライブラリ
- amazon-cognito-identity-js
- aws-sdk
※vue-cli での始め方は、下記を参考にして下さい。 ▶ Vue.js を vue-cli を使ってシンプルにはじめてみる
AWS上Cognitoの設定
ユーザープールの作成
- AWSコンソールにログイン
- Cognitoへ
- 「ユーザープールの管理」をクリック
- 右上「ユーザープールを作成する」をクリック
- 好きなプール名を入力して「デフォルトを確認する」をクリック
- 左のメニューの属性をクリックし、必要に応じて設定を変更
- 左メニューのポリシーでは、パスワードの強度の要求の設定や、有効期限を設定します。また、ユーザーが自分でユーザー登録できるようにするには、ユーザーに自己サインアップを許可するを選択します
- MFAを使うなら、MFAそして確認メニューで設定します
- 実稼働になったら、メッセージのカスタマイズが必要になりそうです。サインアップやパスワードリセット等の際、送られるメールの本文を編集できます
- ユーザーのデバイスを記憶するなら、デバイスの設定から確認してください
- アプリクライアントで、アプリクライアントの追加をします。「クライアントのシークレット」のチェックを外します、あとは、そのまま作成してください。
- これで、プールの作成をします。
ここまでで、 プールIDと、アプリクライアントIDをメモっておきます。
フェデレーティッドアイデンティティの作成をします
- フェデレーティッドアイデンティティの作成
- 新しいプールIDの作成
- プール名と、「認証されていない ID に対してアクセスを有効にする」にチェック
- 認証プロバイダーのCognitoの部分で、先程のメモのユーザープールIDと、アプリクライアントIDを入れる
- 次に進み、詳細を表示から、そのままロールを作成します。
- 作成後表示されるコードをJavascriptを選択してそのコードをコピーしておきます。
ここまで来たらクライアント側に行きましょう
これまでの内容は ▶ 参考Angular+Cognitoのユーザー認証付きSPAのサンプル を参考にして下さい。
ライブラリのインストール
$ yarn add aws-sdk
$ yarn add amazon-cognito-identity-js
cognito設定ファイル
先程メモした設定ファイルを用意します
-
src/congito.js
export default { AWSConfig: { Region: 'ap-northeast-1', UserPoolId: 'ap-northeast-1_XXXXXXXXX', ClientId: 'YYYYYYYYYYYYYYYYYYYYYYYYYY', IdentityPoolId: 'ap-northeast-1:XXXXXXXX-YYYY-XXXX-YYYY-XXXXXXXXXXXX' } }
こんな感じで設定しました。
Cognitoの処理ファイル
Cognito関連(signin, signout, signup, password reset等)の処理をプラグインとしてファイルを作成します。 参考にした記事のもと、src/cognito/以下にファイルを用意します
- src/cognito/cognito.js
import {
CognitoUserPool,
CognitoUser,
AuthenticationDetails,
CognitoUserAttribute
} from 'amazon-cognito-identity-js'
import { Config, CognitoIdentityCredentials } from 'aws-sdk'
import store from '../store'
export default class Cognito {
configure (config) {
if (config.userPool) {
this.userPool = config.userPool
} else {
this.userPool = new CognitoUserPool({
UserPoolId: config.UserPoolId,
ClientId: config.ClientId
})
}
Config.region = config.region
Config.credentials = new CognitoIdentityCredentials({
IdentityPoolId: config.IdentityPoolId
})
this.options = config
this.currentUser = false
}
static install = (Vue, options) => {
Object.defineProperty(Vue.prototype, '$cognito', {
get () { return this.$root._cognito }
})
Vue.mixin({
beforeCreate () {
if (this.$options.cognito) {
this._cognito = this.$options.cognito
this._cognito.configure(options)
}
}
})
}
// サインアップ
signUp (username, password) {
const name = { Name: 'name', Value: username }
const email = { Name: 'email', Value: username }
const now = Math.floor(new Date().getTime() / 1000)
const upatedAt = { Name: 'updated_at', Value: String(now) }
const attributeList = []
attributeList.push(new CognitoUserAttribute(name))
attributeList.push(new CognitoUserAttribute(email))
attributeList.push(new CognitoUserAttribute(upatedAt))
return new Promise((resolve, reject) => {
this.userPool.signUp(username, password, attributeList, null, (err, result) => {
if (err) {
reject(err)
} else {
resolve(result)
}
})
})
}
// サインアップ時のコード認証
confirmation (username, confirmationCode) {
const userData = { Username: username, Pool: this.userPool }
const cognitoUser = new CognitoUser(userData)
return new Promise((resolve, reject) => {
cognitoUser.confirmRegistration(confirmationCode, true, (err, result) => {
if (err) {
reject(err)
} else {
resolve(result)
}
})
})
}
// サインイン
signin (username, password) {
const userData = { Username: username, Pool: this.userPool }
const cognitoUser = new CognitoUser(userData)
const authenticationData = { Username: username, Password: password }
const authenticationDetails = new AuthenticationDetails(authenticationData)
return new Promise((resolve, reject) => {
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: (result) => {
resolve(result)
},
onFailure: (err) => {
reject(err)
}
})
})
}
// サインアウト
signout () {
if (this.userPool.getCurrentUser()) {
this.userPool.getCurrentUser().signOut()
}
}
// 認証ずみかどうか
isAuthenticated () {
this.currentUser = this.userPool.getCurrentUser()
return new Promise((resolve, reject) => {
if (this.currentUser === null) { reject(this.currentUser) }
this.currentUser.getSession((err, session) => {
if (err) {
reject(err)
} else {
if (!session.isValid()) {
reject(session)
} else {
resolve(session)
}
}
})
})
}
// 属性の取得
getAttribute () {
return new Promise((resolve, reject) => {
this.currentUser.getUserAttributes((err, result) => {
if (err) {
reject(err)
}
else {
resolve(result)
}
})
})
}
// コードの再送
resentCode () {
return new Promise((resolve, reject) => {
this.currentUser.getAttributeVerificationCode("email", {
onSuccess: (result) => {
console.log('success getAttributeVerificationCode')
resolve(result)
},
onFailure: (err) => {
console.log('failed getAttributeVerificationCode: ' + JSON.stringify(err))
reject(err)
}
})
})
}
// Eメールアドレス変更後 emailを有効可する
verifyAttribute (confirmationCode) {
return new Promise((resolve, reject) => {
this.currentUser.verifyAttribute("email", confirmationCode, {
onSuccess: (result) => {
console.log('email verification success')
var user = store.getters.user
user["email_verified"] = "true"
store.commit('setUser', user)
resolve(result)
},
onFailure: (err) => {
console.log('email verification failed')
reject(err)
}
})
})
}
// Eメールアドレスの更新
updateEmailAddress (email) {
let attributes = {
'email': email,
'name': email
}
return new Promise((resolve, reject) => {
this.updateAttributes(attributes)
.then(result => { // eslint-disable-line
resolve(result)
var user = store.getters.user
user["email_verified"] = "false"
store.commit('setUser', user)
})
.catch(err => {
reject(err)
})
})
}
// パスワード更新
updatePassword(oldPassword, newPassword) {
return new Promise((resolve, reject) => {
this.currentUser.changePassword(oldPassword, newPassword, (err, result) => {
if (err) {
reject(err)
}
else {
resolve(result)
}
})
})
}
// パスワード忘れメール送信
forgetPassword(username) {
const userData = { Username: username, Pool: this.userPool }
const cognitoUser = new CognitoUser(userData)
return new Promise((resolve, reject) => {
cognitoUser.forgotPassword({
onSuccess: (result) => {
console.log('email verification success')
resolve(result)
},
onFailure: (err) => {
console.log('email verification failed')
reject(err)
}
})
})
}
// パスワードリセット
resetPassword(username, newPassword, code) {
const userData = { Username: username, Pool: this.userPool }
const cognitoUser = new CognitoUser(userData)
return new Promise((resolve, reject) => {
cognitoUser.confirmPassword(code, newPassword, {
onSuccess: (result) => {
console.log('password reset success')
resolve(result)
},
onFailure: (err) => {
console.log('password reset failed')
reject(err)
}
})
})
}
// プロフィール更新
updateAttributes (attributes) {
const attributeList = []
for(var key in attributes) {
const attribute = { Name: key, Value: attributes[key] }
attributeList.push(new CognitoUserAttribute(attribute))
}
return new Promise((resolve, reject) => {
if (this.currentUser === null) { reject(this.currentUser) }
// update attributes
this.currentUser.updateAttributes(attributeList, (err, result) => {
if (err) {
reject(err)
} else {
var user = store.getters.user
for(var key in attributes) {
user[key] = attributes[key]
}
store.commit('setUser', user)
resolve(result)
}
})
})
}
}
大事なところは、installメソッドでVueプラグインとして記述している部分です。 また、ちょこちょこstoreが出てきますが、vuexでstoreを用意してます。
- index.js
import Vue from 'vue'
import Cognito from './cognito'
import config from './../config'
Vue.use(Cognito, config.AWSConfig)
export default new Cognito()
cognito関連処理をvueのプラグインとして読みます
- src/main.js
import Vue from 'vue'
import App from './App'
import router from './router'
import cognito from './cognito'
import store from './store'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
cognito,
store,
template: '<App/>',
components: { App }
})
- src/store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
user: null,
},
getters: {
user: state => state.user,
},
mutations: {
// ユーザー情報保存
setUser(state, user) {
state.user = user
}
}
});
export default store
ログイン状況や、プロフィール内容をstoreで保持しています。
次にルーターです
- src/router.js
import Vue from 'vue'
import Router from 'vue-router'
import cognito from './cognito'
import store from './store'
import {
Signin,
Signup,
Confirm,
ForgetPassword,
ResetPassword,
Mypage,
Top,
About,
} from './components/pages'
Vue.use(Router)
const signout = (to, from, next) => {
cognito.signout()
next('/signin')
}
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'top',
component: Top
},
{
path: '/about',
name: 'about',
component: About,
meta: { requiredAuth: true }
},
{
path: '/signin',
name: 'Signin',
component: Signin,
meta: { notRequiredAuth: true }
},
{
path: '/reset-password',
name: 'ResetPassword',
component: ResetPassword,
meta: { notRequiredAuth: true }
},
{
path: '/forget-password',
name: 'ForgetPassword',
component: ForgetPassword,
meta: { notRequiredAuth: true }
},
{
path: '/signup',
name: 'Signup',
component: Signup,
meta: { notRequiredAuth: false }
},
{
path: '/confirm',
name: 'Confirm',
component: Confirm,
meta: { notRequiredAuth: false }
},
{
path: '/mypage',
name: 'Mypage',
component: Mypage,
meta: { requiredAuth: true }
},
{
path: '/signout',
beforeEnter: signout,
},
{
path: '*',
component: NotFound
},
]
})
router.beforeResolve(async(to, from, next) => {
// Get signin session.
cognito.isAuthenticated()
.then(session => { // eslint-disable-line
const token = session.idToken.jwtToken
// get attribute.
cognito.getAttribute()
.then(result => {
var user = {}
for(var v of result) {
user[v.getName()] = v.getValue()
}
user['token'] = token
store.commit('setUser', user)
})
.catch(error => {
store.commit('setUser', null)
console.log(error)
signout(to, from, next)
})
if (to.matched.some(record => record.meta.notRequiredAuth)) {
next({
path: '/',
})
}
})
.catch(error => { // eslint-disable-line
store.commit('setUser', null)
if (to.matched.some(record => record.meta.requiredAuth)) {
next({
path: '/signin',
query: { redirect: to.fullPath }
})
}
})
next()
})
export default router
beforeResolveにて、毎回sessionのチェックをしてログイン状態であれば、プロフィール情報やtokenを取得してます。
Sign in
参考までに、signin.vueを記載しときます
- signin.vue
<template>
<div class="signin">
<b-container fluid>
<b-row>
<b-col offset-md="4" md="4">
<h3>{{ $t('signin.title') }}</h3>
<b-form>
<b-alert v-if="error" show variant="danger">{{ $t('signin.signin-error') }}</b-alert>
<b-form-group
:label="$t('signin.email')"
label-for="email"
:invalid-feedback="invalidEmailFeedback"
:valid-feedback="validEmailFeedback"
:state="emailValidation"
>
<b-form-input id="email" :placeholder="$t('signin.email-placeholder')" v-model="email" :state="emailValidation" trim></b-form-input>
</b-form-group>
<b-form-group
:label="$t('signin.password')"
label-for="password"
:invalid-feedback="invalidPasswordFeedback"
:valid-feedback="validPasswordFeedback"
:state="passwordValidation"
>
<b-form-input type="password" id="password" :placeholder="$t('signin.password-placeholder')" v-model="password" :state="passwordValidation" trim></b-form-input>
</b-form-group>
<b-button type="button" variant="outline-dark" @click="signin">
{{ $t('signin.title') }}
<span v-if="loading" class="spinner-grow spinner-grow-sm" role="status" aria-hidden="true"></span>
</b-button>
<div class="forget-password">
<font-awesome-icon icon="lock" />
<b-link to="/forget-password"> {{ $t('signin.forget-password') }}</b-link>
</div>
</b-form>
</b-col>
<b-col md="4" />
</b-row>
</b-container>
</div>
</template>
<script>
export default {
name: 'Signin',
data () {
return {
email: '',
password: '',
error: false,
loading: false
}
},
computed: {
emailValidation() {
return this.validEmail()
},
invalidEmailFeedback() {
return this.$t('signin.email-error')
},
validEmailFeedback() {
return this.emailValidation === true ? 'OK' : 'InValid'
},
passwordValidation() {
return this.validPassowrd()
},
invalidPasswordFeedback() {
return this.$t('signin.password-error')
},
validPasswordFeedback() {
return this.passwordValidation === true ? 'OK' : 'InValid'
}
},
methods: {
signin () {
if (this.emailValidation && this.passwordValidation) {
this.loading = true
this.$cognito.signin(this.email, this.password)
.then(result => { // eslint-disable-line
var url = this.$route.query.redirect
if (url) {
this.$router.replace(url)
}
else {
this.$router.replace('/')
}
})
.catch(err => {
console.log(err)
this.error = true
this.loading = false
})
}
else {
this.error = true
}
},
validEmail () {
var re = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
return this.email ? re.test(this.email) : null
},
validPassowrd() {
return this.password ? this.password.length > 7 : null
}
}
}
</script>
<style lang="scss">
.signin {
h3 {
margin-bottom: 30px;
}
.forget-password {
margin-top: 20px;
font-size: 15px;
}
}
</style>
VueのBootstrapを使ってます。とi18nで多言語対応もしてます。 その他のsignup等も同じ要領でcomponentとして用意していきます。
株式会社UKAI 代表取締役CEO。建築を専攻していたが、新卒でYahoo!Japanにエンジニアとして入社。その後、転職、脱サラ、フリーランスエンジニアを経て、田舎へ移住し、ゲストハウスの開業、法人成り。ゲストハウス2軒、焼肉店、カフェ、食品製造業を運営しています。詳細はこちらから