2020年3月7日に投稿

【Firebase Auth】Nuxt.jsでリロード時に認証情報を再取得する方法(SPA編)

どうも。VueにハマりまくりのTack-Tです。
今回はFirebase Authenticationを使った認証機能の実装について書いてみたいと思います。

Firebase Authentication便利ですよね。
ただ、Vuexにユーザ情報とか保持させているとリロード時に気を付けてあげる必要があります。
今回はこの部分にフォーカスを当ててみます。
おそらく長くなるのでSPA編とSSR編に分けて連載にしようと思います。

この記事を読む前に

今回はFirebaseUIを使いますが導入や設定については本記事では触れません。
理由は優良な記事が他にたくさん存在するからです。
また、以下の資料には必ず目を通しておいてください。

ことのはじめ

今回のテーマの例として以下の機能を持ったアプリを作ってみます。(アプリとは呼べない代物ですが)

  • FirebaseUIを使ってログインページを作る
  • 認証が完了したユーザの情報をvuexstoreに保存する
  • マイページを用意する。ログイン後にリダイレクトされる

最低限の下準備

ここではFirebaseに接続するために最低限必要な準備だけしておくことにします。

プロジェクトのルートディレクトリ直下にfirebase.config.jsを作成します。
必要な情報はすべて自分のFirebaseのプロジェクトページから取得できます。

firebase.config.js

export default {
  apiKey: "enter your API Key",
  authDomain: "your domain",
  databaseURL: "your database URL",
  projectId: "your Project ID",
  storageBucket: "your Storage",
  messagingSenderId: "0000000000"
}

次にこの設定を読み込こませたfirebaseインスタンスを作るプラグインを作成します。

/plugins/firebase.js

import firebase from 'firebase'
import config from '~/firebase.config';

if (!firebase.apps.length) {
  firebase.initializeApp(config)
}

export const db = firebase.firestore()
export default firebase

あとはコンポーネント側で必要な時にimportして使います。

SPAモードで作ってみる

では早速SPAモードで画面を作っていきましょう。

ログイン機能

ログイン処理の流れとしては以下のようになっています。

  1. 認証が完了する
  2. storeにユーザーの情報を保存する
  3. マイページにリダイレクトする

まずはログインページを実装します。
よくあるonAuthStateChangedを使った方法です。

pages/login.vue

<template>
  <div>
    <div id="firebaseui-auth-container"></div>
  </div>
</template>

<script>
import firebase from '@/plugins/firebase'
import { mapActions } from 'vuex'

export default {
  mounted() {
    const firebaseui = require('firebaseui');
      firebase.auth().onAuthStateChanged(user => {
        if (!user) {
          const ui = firebaseui.auth.AuthUI.getInstance() || new firebaseui.auth.AuthUI(firebase.auth())
          const config = {
            signInOptions: [
              firebase.auth.EmailAuthProvider.PROVIDER_ID,
              firebase.auth.GithubAuthProvider.PROVIDER_ID
            ],
            callbacks: {
              signInSuccessWithAuthResult: (authResult, redirectUrl) => {
                // あとでstoreのactionsで定義する
                this.signIn(authResult.user)
                return false
              }
            },
            signInFlow: 'popup'
          }
          ui.start('#firebaseui-auth-container', config)
        } else {
          this.$router.push(`/users/${user.uid}`)
        }
      })
  },
  methods: {
    ...mapActions('modules/user', ['signIn'])
  }
}
</script>

<style>

</style>

ポイントはcallbacks内のsignInSuccessWithAuthResultで定義している関数です。
認証が完了したタイミングで実行されるので、ここで認証結果から必要な情報を取り出してstoreに保存するようにします。

今はまだstoreもactionsも定義していないので作っていきましょう。

store/modules/user.js

import { db } from '@/plugins/firebase'

export const strict = false

export const state = () => ({
  user: {}
})

export const getters = {
  // ログイン済み判定
  isAuthenticated (state) {
    return !!Object.keys(state.user).length
  }
}

export const mutaions = {
  setUser (state, payload) {
    state.user = payload
  }
}

export const actions = {
  setUser ({ commit }, payload) {
    commit('setUser', payload)
  },
  signOut ({ commit }) {
    firebase.auth().signOut()
      .then(() => {
        commit('setUser', {})
        console.log('ログアウトしました')
      })
      .catch(err => {
        console.error(err)
      })
  },
  signIn ({ dispatch }, user) {
    return new Promise((resolve, reject) => {
      db.collection('users').doc(user.uid).get()
      .then(docSnapshot => {
        if (!docSnapshot.exists) {
          const userInfo = {
            name: user.displayName,
            uid: user.uid
          }
          // 初回ログイン時のみユーザー情報をFirestoreのドキュメントとして新規登録
          db.collection('users').doc(user.uid).set(userInfo)
        }
        dispatch('setUser', {
          name: user.displayName,
          email: user.email,
          uid: user.uid
        })
        resolve(user)
      })
      .catch(err => reject(err))
    })
  }
}

モジュールモードで記述しています。
必要最低限の実装のみですが、acitonssignInが認証完了時に呼ばれる関数になります。
今回はユーザーの認証情報をFirestoreに保存するようにしています。
なんとなくPromiseを返していますが別に返さなくても大丈夫です。

最後にログイン後にリダイレクトされるマイページを作ります。

pages/users/_uid.vue

<template>
  <div>
    <h1 class="text-center">{{ `ここは${user.name}のページ` }}</h1>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState('modules/user', ['user'])
  }
}
</script>

これでログイン機能については実装完了です。
試しにどんな認証でも良いので試してみてください。Firestoreにデータが登録され、マイページにもリダイレクトされるはずです。

リロードに耐える

しかし、今のままではマイページでリロードを行うと、storeの値がリセットされるのでユーザ名が表示されないはずです。
次はこのリロード問題を解決します。
対策は簡単で、middlewareを使ってDOMが生成される前にstoreに値を再設定してあげればOKです。

middleware/reloadCredential.js

import firebase from '@/plugins/firebase'

export default ({ store }) => {
  if (process.client) {
    if (!store.getters['modules/user/isAuthenticated']) {
      firebase.auth().onAuthStateChanged((user) => {
        if (user) {
          store.dispatch('modules/user/signIn', user)
        }
      })
    }
  }
}

onAuthStateChangedメソッドを使えば認証済みかどうかを判断できます。
戻り値の認証結果のデータにはユーザ情報が含まれているので、これを先ほどのsignInにディスパッチします。
あとはこのmiddlewareをサイト全体に適用するためにnuxt.config.jsに登録しましょう。

nuxt.config.js

import colors from 'vuetify/es5/util/colors'
import ja from 'vuetify/es5/locale/ja.js'

export default {
  // ~中略~
  router: {
    middleware: 'reloadCredential'
  },
  // ~中略~
}

これでリロード時にstoreにユーザ情報を入れることができるようになりました。
SPAモードの場合これだけでリロード時のstoreへの再設定ができるようになります。
次回はSSRモードの場合です。
SSRの場合はサーバ側の処理を考慮する必要があるためさらに工夫が必要になります。
おたのしみに。

それではまた次回。