概要
昨今、バックエンドがAPIサーバーであることも増えてきたが、認証機能の実装は面倒なことが多い。そのため、認証機能をFirebase Authenticationに任せることも考えられる。本記事では、人気のJavaScriptフレームワークであるVue.jsでフロントエンドのログイン機能を実装し、Firebase Authenticationと連携させる。
また、Vuexを用いたユーザー情報などの状態管理、vue-routerによるルーティングを行い、UIコンポーネントフレームワークとしてVuetifyを使用した。
本記事で説明するシステムは以下の通りです。"/"でホーム画面が描画され、ログイン状態のとき"/test"でもホーム画面が描画される。一方で、ログインしていないときの"/test"では、"/signin"にリダイレクトされログイン画面が表示される。また、"/signup"でサインアップが描画される。
ログイン、サインアップは、firebaseとの連携により実行される。また、ログイン状態は、Vuexにより管理される。
本記事のサンプルは、以下のバージョンを用いた。
npm install -g @vue/cli
vue --version
> 3.10.0
git: k-washi/example-vue-cliの一部が本記事に対応しています。
Vue.jsの設定
本章では、Vue.jsのプロジェクトを作成し、Vuex, vue-router, vuetifyを用いて作成されたプロジェクトを起動させます。
1. プロジェクトの作成
以下のコマンドでプロジェクトを作成する。このとき、Vuex, vue-routerも同時にダウンロードする。
#projectの作成
vue create sample-vue
# 以下、選択肢 (例なので適宜変更してください)
#>Mannually
#>Vuex, Routerを選択
#(他にも必要なパッケージがあれば選択する。選択例 Babel, Router, Vuex, Linter)
#U>se history mode for router / Y (default)
#>ESLint * Prettier
#>Lint on save
#>In package.json
2. Vuetifyのダウンロード
#package.jsonがあるディレクトリにて
vue add vuetify
#>Default
#もし iconを使用するなら
npm install @mdi/font -D
npm install @mdi/js -D
3. プロジェクトの実行とビルド
以下のコマンドでプロジェクトを実行し、ブラウザでApp running atに記載されたIPにアクセスする。
npm run serve
また、以下のコマンドでビルドすることで、distファイルの配下にindex.htmlやcssファイルなどが作成される。
npm run build
以下の画面が描画結果です。
Firebaseの設定
Firebaseのプロジェクト設定
- Firebaseにてアカウントを作成し、コンソールに移動する。
- プロジェクトを追加する。
- Project Overviewの>マークからアプリを登録する (Firebase Hostingはチェックしない)
- Authenticationのログイン方法を設定(今回は、メール/パスワードを有効にする)
Vue.jsにインストール
npm install firebase --save
ユーザー管理処理の実装
まず、前提として、emailとパスワードを用いてfirebase側で認証される。そして、その結果、改善検知が可能なJWTが得られる。
ここでは、firebaseと連携したユーザー管理機能として、ユーザーの作成、ログイン、ログアウト、状態確認の機能を実装した。
Vuexを用いたユーザーのログイン状態管理
stateに管理する状態を列挙する。そして、この状態を取得するgettersと状態を更新するmutationsを実装する。
ここでは、emailとログイン状態を管理することとする。
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
strict: true,
state: {
email: "",
status: false,
},
getters: {
email(state) {
return state.email
},
isSignedIn(state) {
return state.status
},
},
mutations: {
onAuthEmailChanged(state, email) {
state.email = email; //firebase user情報
},
onUserStatusChanged(state, status) {
state.status = status;
}
},
});
Firebaseとの連携部分
まずは、Firebaseと連携するための設定を行う。Project Overviewプロジェクト設定のFirebase SDK snippetに記載されているfirebaseConfigをsample-vue/.env.localに記載する。この.envファイルは、VUE_APPから開始する変数を設定できる。
VUE_APP_FB_KEY=AIzaxxxxxxxxxx
VUE_APP_FB_DOMEIN=ex-firebase-auth
VUE_APP_FB_ID=https://ex-firebase-auth
VUE_APP_FB_PROD_ID=https://ex-firebase-auth
VUE_APP_FB_BUCKET_ID=ex-firebase-auth.appspot.com
VUE_APP_FB_SENDER_ID=661xxx18x2
次に、firebaseと連携する処理を実装する。
詳細はコード内に記載。
import firebase from "@firebase/app";
import "@firebase/auth";
import router from "./router"
import store from "./store";
import { createSecretKey } from "crypto";
//process.envでenvファイルに定義した変数にアクセスできる
const firebaseConfig = {
apiKey: process.env.VUE_APP_FB_KEY,
authDomain: process.env.VEW_APP_FB_DOMEIN + ".firebaseapp.com",
databaseURL: process.env.VUE_APP_FB_ID + ".firebaseio.com",
projectId: process.env.VUE_APP_FB_PROD_ID,
storageBucket: process.env.VUE_APP_FB_BUCKET_ID,
messagingSenderId: process.env.VUE_APP_FB_SENDER_ID,
};
export default {
//読み込み時に、firebase機能の設定をする
init() {
firebase.initializeApp(firebaseConfig);
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION);
},
//emailとパスワードでログインする。
//firebase認証の結果、JWT Tokenが返ってくる。JWTは、localstrageに保存する。
//ここで、記載しているrouterは、vue-routerの機能で、'/'へルーティングしている
signInWithEmailAndPassword(email, password) {
firebase.auth().signInWithEmailAndPassword(email, password)
.then(res => {
res.user.getIdToken().then(idToken => {
localStorage.setItem('jwt', idToken);
router.push('/').catch(err => {
console.log("router push /");
});
})
}, err => {
console.log(err.message);
})
},
//emailとパスワードでアカウント作成する
//アカウント作成後は、'/signin'へルーティング
signUpWithEmailAndPassword(email, password) {
firebase.auth().createUserWithEmailAndPassword(email, password);
.then(res => {
router.push('/signin');
}).catch(err => {
console.log(err.message);
})
},
//ログアウト
//ログアウト後は、保存しているjwtを削除して、vuexのmutationに実装した状態の更新処理でユーザーをログアウト状態にする。
logOut() {
firebase.auth().signOut()
.then(() => {
localStorage.removeItem("jwt")
store.commit('onAuthEmailChanged', "");
store.commit('onUserStatusChanged', false);
})
.catch((err) => {
console.log(`fail logout (${error}) `);
})
},
//状態管理
//jwtの状態、ユーザーの状態を更新する。
onAuth() {
firebase.auth().onAuthStateChanged(user => {
if (user) {
if (user.ma) {
localStorage.setItem('jwt', user.ma);
}
store.commit('onAuthEmailChanged', user.email);
if (user.uid) {
store.commit('onUserStatusChanged', true);
} else {
store.commit('onUserStatusChanged', false);
}
} else {
store.commit('onAuthEmailChanged', "");
store.commit('onUserStatusChanged', false);
}
})
}
}
次にfirebaseとの連携処理にも出てきたルーティング機能を実装する。
今回は、簡単のため、ログイン状態でアクセスしなければならないパスを/testとし、ログイン後は/testでホーム画面が描画されるものとする。
routersにpathとともに、描画するコンポーネントを記載することで, コンポーネント内に記載された<router-view></router-view>
の場所にルーティングのコンポーネントを描画する。
path:"*"は、一致するpathがなかった場合のリダイレクトのために設定している。
scrollBehaviorは、戻る処理をしたときに、表示する位置を遷移以前の位置で再度描画するための処理を行う。
router.beforeEacは、ルーティング時のミドルウェアの役割であり、ルーティング処理が実行されるたびに呼び出される。Firebase.onAuth()でユーザーの状態を確認し、選択されたパスへルーティングする。また、meta: {requiresAuth: true}が付随したパスであり、かつ、未ログイン状態の場合、ログイン画面へルーティングされる。
import Vue from "vue";
import Router from "vue-router";
import Home from "@/views/Home.vue";
import Signup from "@/components/FBSignup.vue";
import Signin from "@/components/FBSignin.vue"
import Firebase from "./firebase"
import store from "./store"
Vue.use(Router);
const router = new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home
},
{
path: "/test",
name: "test",
component: Home,
meta: {requiresAuth: true},
},
{
path: "/signup",
name: "signUp",
component: Signup,
},
{
path: "/signin",
name: "signIn",
component: Signin,
},
{
path: "*",
redirect: '/'
},
],
scrollBehavior(to, from, savedPosition) {
//戻るによる遷移は以前の位置を保持
if(savedPosition) {
return savedPosition
} else {
if (to.hash) {
return {selector: to.hast}
} else {
return {x: 0, y: 0}
}
}
}
});
router.beforeEach((to, from, next) => {
Firebase.onAuth();
let currentUserStatus = store.getters["isSignedIn"];
let requiresAuth = to.matched.some(record => record.meta.requiresAuth);
if (!requiresAuth ) {
next()
} else if (requiresAuth && !currentUserStatus) {
next('/signin');
} else {
next();
}
})
export default router
最初に読み込んだときに実行する処理をmain.jsに実装した。
ここでは、firebaseの初期化をするとともに、vuex, vue-router, vuetifyをアプリに登録している。
また、firebase.auth().onAuthStateChanged()は、ブラウザの更新を行ったときなどに、非同期に実行されるユーザー情報の更新を描画前に実行するために実装している。
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import vuetify from "./plugins/vuetify";
import firebase from "firebase/app"
Vue.config.productionTip = false;
Firebase.init();
firebase.auth().onAuthStateChanged(user => {
if (user) {
if (user.ma) {
localStorage.setItem('jwt', user.ma);
}
store.commit('onAuthEmailChanged', user.email);
if (user.uid) {
store.commit('onUserStatusChanged', true)
} else {
store.commit('onUserStatusChanged', false)
}
} else {
store.commit('onAuthEmailChanged', "")
}
new Vue({
router,
store,
vuetify,
render: h => h(App)
}).$mount("#app");
})
最後に、ログイン画面のコンポーネントについて説明する。
本コンポーネントは、ログイン状態の場合、ユーザー情報が描画され、ログイン状態ではない場合、ログインフォームが描画される。userStatusでユーザーのログイン状態を取得しており、usernameでユーザーのemail情報を取得している。
v-*はvuetifyによるコンポーネントです。
<v-btn class="ma-2" outlined color="deep-orange" to="/">
はボタンコンポーネントでto="/"でボタンを押した際に移動するパスを指定している。
また、<v-btn class="ma-2" outlined color="deep-orange" @click="signIn">Sign In</v-btn>
はクリック時に、methodsに定義したsignInメソッドを実行する。signInは単純にFirebase.jsに定義したログイン関数を使用している。(本当は、フォーム内容で条件分岐が必要かもしれません...)
<template>
<div>
<v-btn class="ma-2" outlined color="deep-orange" to="/">
Home
</v-btn>
<p>アカウントを持っていない場合</p>
<v-btn class="ma-2" outlined color="deep-orange" to="/signup">
Sign Up
</v-btn>
<div v-if="userStatus" key="login">
<h2>{{username}} がログイン済みです</h2>
</div>
<div v-else key="login_process">
<h2>Sign In</h2>
<v-form ref="form" v-model="valid" :lazy-validation="lazy">
<v-col cols="12" sm="6">
<v-text-field
v-model="email"
label="email"
type="email"
required
></v-text-field>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
v-model="password"
label="password"
type="password"
hint="At least 8 characters"
:rules="[rules.required, rules.min]"
required
></v-text-field>
</v-col>
<v-btn class="ma-2" outlined color="deep-orange" @click="signIn">Sign In</v-btn>
</v-form>
</div>
</div>
</template>
<script>
import Firebase from "../firebase"
export default {
name: 'Signin',
data: () => ({
email: '',
password: '',
rules: {
required: v => !!v||'Required.',
min: v => v.length >= 8||'Min 8 characters',
},
valid: true,
lazy: false,
}),
computed: {
username() {
return this.$store.getters.email
},
userStatus() {
//return true in login state
return this.$store.getters.isSignedIn
},
},
methods: {
signIn() {
Firebase.signInWithEmailAndPassword(this.email, this.password);
},
},
}
</script>
Appendix
アカウント作成画面などは、ログイン画面と酷似しているため説明を省いた。今回の内容と一致しないが、追加で、k-washi/example-vue-cliに、様々な処理を実装しているので参考にしてみてください。