【Vue.js×Firabase】メールログイン機能(パスワード認証)を実装する手順


Vue.js と Firebase でちょっとした Sigle Page Application を開発する際にログイン機能が必要になる事はよくあります。

今回は下記の手順でログイン機能を実装する方法について解説していきます。

  1. ログイン時にサーバーからトークンを受け取る。
  2. localStorage などに保存。
  3. トークンを含めたリクエストを送って判定。

利用する技術としては Vue.js / Vuex / Vue Router / Firebase Authentication です。

画面の作成

まずはログイン画面と登録画面を作成する。vue-router で画面の切り替えを制御するため router.js を作成。

import Vue from 'vue';
import Router from 'vue-router';
import Comments from './views/Comments.vue'
import Login from './views/Login.vue'
import Register from './views/Register.vue'

Vue.use(Router);

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      component: Comments
    },
    {
      path: '/login',
      component: Login
    },
    {
      path: '/register',
      component: Register
    }
  ]
});

掲示板の画面とログイン画面と会員登録画面を作る。

Firebase Authentication のセットアップ

Firebase の Authentication のページで「始める」をクリックする。Sign-in method のタブが表示されるので利用する認証方法を選ぶ。今回は「メール / パスワード」を選んで有効にします。

ユーザーの登録処理を実装

Firebase Auth REST API のドキュメントにエンドポイントのURLが書いてあるのでそれをコピーしておきます。(メール/パスワード > サインアップ を参照)

エンドポイントの API_KEY は「Firebase のプロジェクトを設定」から確認。

ログイン時と会員登録時のエンドポイントは同じURLなので axios-auth.js というファイルを作り定義しておきます。

import axios from 'axios';

const instance = axios.create({
  baseURL: 'https://identitytoolkit.googleapis.com/v1/'
})

export default instance;

登録画面(Register.vue)で登録時の処理。

import axios from '../axios-auth';
・・・
methods: {
  register() {
    axios.post(
      '/accounts:signUp?key=API_KEY',   // API_KEY はプロジェクトごとに違う
      {
        email: this.email,
        password: this.pass,
        returnSecureToken: true
      }
    )
    .then(response => {
      this.email = '';
      this.pass = '';
    });
  }

画面からユーザー登録すると Firebase Authentication にユーザーが追加できるはずです。
パスワードの文字数が少ないとエラーになるので8文字とかが無難。

ログイン処理を実装

Firebase Auth REST API のドキュメントにエンドポイントのURLが書いてあるのでそれをコピーしておきます。(メール/パスワード > サインイン を参照)
ログイン画面に処理を追加。URLの前半は axios-auth.js で定義したものを使えます。

import axios from '../axios-auth';
・・・
methods: {
  login() {
    axios.post(
      '/accounts:signInWithPassword?key=API_KEY',
      {
        email: this.email,
        password: this.pass,
        returnSecureToken: true
      }
    )
    .then(response => {
      console.log(response);
      this.email = '';
      this.pass = '';
    });
  }

ログイン画面からログイン。

responsestatus: 200 が返ってこればログイン成功です。

ログインユーザーのみ掲示板を表示

ユーザーIDがセットされている場合のみデータ取得を許可するように Firestore を設定します。
Firestore の「ルール」タブからルールの編集。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth.uid != null;
    }
  }
}

これでユーザIDがない場合(未ログイン)はデータ取得できないようになります。

次に Vuex を使ってログイン時に取得したユーザーIDをデータ取得時に送るようにする。store/index.js を作成して state , getters を定義。

import Vue from 'vue';
import Vuex from 'vuex';
import axios from '../axios-auth';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    idToken: null
  },
  getters: {
    idToken: state => state.idToken
  },

mutations, actions を使ってログイン時にユーザーIDをセット。
store/index.jsviews/Login.vue を編集。

mutations: {
  updateIdToken(state, idToken) {
    state.idToken = idToken;
  }
},
actions: {
  login({ commit }, authData) {
    axios.post(
      '/accounts:signInWithPassword?key=AIzaSyCARQyIS_clgR5qqghwZ78YJ0tl2dhqdLA',
      {
        email: authData.email,
        password: authData.pass,
        returnSecureToken: true
      }
    )
    .then(response => {
      commit('updateIdToken', response.data.idToken);
    });
  },
methods: {
  login() {
    this.$store.dispatch('login', {
      email: this.email,
      pass: this.pass
    })
    this.email = '';
    this.pass = '';
  }
}

これでログイン時に idToken を取得して state.idToken にセットできるようになりました。

データ取得時のヘッダーにこの値を含めるために views/Comments.vue を編集。

<h2>掲示板</h2>  
<div v-for="post in posts" :key="post.name">

・・・

computed: {
  idToken() {
    return this.$store.getters.idToken;
  }
},
created() {
  axios.get(
    axios.defaults.baseURL, {
      headers: {
        Authorization: `Bearer ${this.idToken}`
      }
    }
  )
  .then((response) => {
    this.posts = response.data.documents;
  });

これでログイン時は Firestore からデータを取得できるようになります。

ログイン状態に応じた画面制御

ナビゲーションガードを使うとログイン有無によってリダイレクトを制御できます。

未ログイン状態で掲示板画面に来たらログイン画面へリダイレクト。

routes: [
  {
    path: '/',
    component: Comments,
    beforeEnter(to, from, next) {
      if (store.getters.idToken) {
        next();
      } else {
        next('/login');
      }
    }
  },

ログイン時にログイン画面や登録画面に来たら掲示板画面にリダイレクト。

{
  path: '/login',
  component: Login,
  beforeEnter(to, from, next) {
    if (store.getters.idToken) {
      next('/');
    } else {
      next();
    }
  }
},
{
  path: '/register',
  component: Register,
  beforeEnter(to, from, next) {
    if (store.getters.idToken) {
        next('/');
    } else {
        next();
    }
  }
}

ログイン後と登録後は掲示板画面にリダイレクト。

import router from '../router';

・・・

login({ commit }, authData) {
・・・
  .then(response => {
    commit('updateIdToken', response.data.idToken);
    router.push('/');  // リダイレクト
  });
},
register({ commit }, 
・・・
  .then(response => {
    commit('updateIdToken', response.data.idToken);
    router.push('/');  // リダイレクト
  });
}

v-if を使ってリンクの表示も切り替えておきます。
ログイン時は掲示板のみ表示、未ログイン時はログインと登録を表示。リンクは App.vue に書いてあるのでそれを編集します。

<router-link to="/" class="header-item" v-if="isAuthenticated">掲示板</router-link>
<router-link to="/login" class="header-item" v-if="!isAuthenticated">ログイン</router-link>
<router-link to="register" class="header-item" v-if="!isAuthenticated">登録</router-link>

・・・

<script>
export default {
  computed: {
    isAuthenticated() {
      return this.$store.getters.idToken !== null;
    }
  }
}
</script>

ログイン情報を保持

Firebase のトークンは1時間で無効になるので securetoken.googleapis.com を使って新しいIDトークンをリフレッシュするようにします。

login({ dispatch },authData) {
  axios.post(
    '/accounts:signInWithPassword?key=AIzaSyCARQyIS_clgR5qqghwZ78YJ0tl2dhqdLA',
    {
      email: authData.email,
      password: authData.pass,
      returnSecureToken: true
    }
  )
  .then(response => {
    dispatch('setAuthData', {
      idToken: response.data.idToken,
      expiresIn: response.data.expiresIn,
      refreshToken: response.data.refreshToken
    });
    router.push('/');
  });
},
refreshToken({ dispatch }, refreshToken) {
  axiosRefresh
    .post('/token?key=AIzaSyCARQyIS_clgR5qqghwZ78YJ0tl2dhqdLA', {
      grant_type: 'refresh_token',
      refresh_token: refreshToken
    })
    .then(response => {
      dispatch('setAuthData', {
        idToken: response.data.id_token,
        expiresIn: response.data.expires_In,
        refreshToken: response.data.refresh_token
      });
    })
},
・・・
},
setAuthData({ commit, dispatch }, authData) {
  const now = new Date();
  const expiryTimeMs = now.getTime() + authData.expiresIn * 1000;
  localStorage.setItem('idToken', authData.idToken);
  localStorage.setItem('expiryTimeMs', expiryTimeMs);
  localStorage.setItem('refreshToken', authData.refreshToken);
  commit('updateIdToken', authData.idToken);
  setTimeout(() => {
    dispatch('refreshToken', authData.refreshToken);
  }, authData.expiresIn * 1000);
}

ログイン後は idToken などを LocalStrage に入れておきます。
store/index.jsautoLogin() を追加して main.jsdispatch を追加。

mutations: {
  updateIdToken(state, idToken) {
    state.idToken = idToken;
  }
},
actions: {
    autoLogin({ commit, dispatch }) {
       const idToken = localStorage.getItem('idToken');
       if (!idToken) return;
       const now = new Date();
       const expiryTimeMs = localStorage.getItem('expiryTimeMs');
       const isExpired = now.getTime() >= expiryTimeMs;
       const refreshToken = localStorage.getItem('refreshToken');
      if (isExpired) {
        dispatch('refreshIdToken', refreshToken);
      } else {
        const expiresInMs = expiryTimeMs - now.getTime();
        setTimeout(() => {
          dispatch('refreshIdToken', refreshToken);
        }, expiresInMs);
        commit('updateIdToken', idToken);
      }
       commit('updateIdToken', idToken); 
    },
import store from './store';

・・・

store.dispatch('autoLogin');

LocalStrage にデータがあれば autoLogin でその都度ログイン情報が更新されるのでリロードしてもログイン状態が保持される仕組みです。

ログアウト処理を実装

App.vue にログアウトを設置。

<router-link to="/login" class="header-item" v-if="!isAuthenticated">ログイン</router-link>

・・・

	methods: {
	  logout() {
	    this.$store.dispatch('logout');
	  }
	}

store/index.js で処理を追加。

logout({ commit }) {
  commit('updateIdToken', null);
  localStorage.removeItem('idToken');
  localStorage.removeItem('expiryTimeMs');
  localStorage.removeItem('refreshToken');
  router.replace('/login');
},

これでログアウトをクリックしたらログイン情報がクリアされログイン画面遷移するはずです。

まとめ

Vue.js / Vuex / Vue Router / Firebase Authentication を使った SPA でのログイン機能の実装について解説しました。

小規模なアプリであれば Firebase Authentication で十分ですね。

今回の記事がログイン処理の実装の際にお役に立てたら幸いです。

参考

Udemy 超Vue.js 2 完全パック (Vue Router, Vuex含む)