LoginSignup
48

More than 5 years have passed since last update.

Firebase Authentication + Vue.js を SSR/SPA 両方に対応する方法

Posted at

Firebase Authentication は手軽に認証の仕組みが使えてとても良いです。今回は Vue.js を使って SSR (ServerSideRendering) と SPA (SinglePageApplication) 両方に対応した使い方をまとめておきます。

というのも普通の SPA であれば特になんの問題もなく使えるのが SSR にも対応した形にしようとすると、とても苦労したというのがあり自分でまとめておかないと忘れてしまいそうになるからです。

使うもの

  • Vue.js
  • Vue Router
  • Vuex
  • Firebase

SPA で普通に認証をしてみる

Vue.js + Vue Router を使って認証が必要なページを用意し、Firebase Authentication で認証が通っている場合は閲覧できるようにしてみます。ここではサンプルとして Google ログインを使います。ここでは特に難しいことはありません。

router/routes.js

const routes = [
  {
    path: '/',
    component: () => import('layouts/MyLayout.vue'),
    children: [
      {
        path: '',
        name: 'Home',
        component: () => import('pages/Index.vue'),
        meta: { requiresAuth: false }
      },
      {
        path: 'mypage',
        name: 'MyPage',
        component: () => import('pages/MyPage.vue'),
        meta: { requiresAuth: true }
      }
    ]
  }
]

requiresAuth: false というオブジェクトを用意して認証が必要なページかそうでないかを明記しておきます。

router/index.js

次に Vue Router の Router.beforeEach を使ってページ遷移前に認証が必要なページかどうかを見て、アクセスしてきているクライアントの認証状況を確認します。すでに認証されている場合は firebase.auth().currentUser でユーザ情報が確認できます。されていない場合は null が返ってきます。

認証されていない場合は、onAuthStateChanged を使って認証結果をコールバックで待ちます。動作としては非同期でクライアントから Web SDK を通じて Firebase へ認証リクエストが飛んでいる形になります。コールバックで受け取れるものは currentUser を同じです。以降はブラウザのリロードをしない限りは currentUser にデータが入っている状態になります。

Router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // すでに認証済みの場合は遷移
    if (firebase.auth().currentUser) {
      next()
      return
    }
    // まだ認証されていない場合は Firebase SDK からのコールバックを待つ
    firebase.auth().onAuthStateChanged(user => {
      if (user) {
        next()
      } else {
        next({ name: 'Home' })
      }
      return
    })
  }
  next()
})

pages/Home.vue

続いて Google ログインするページを用意します。認証プロバイダのオブジェクトを用意して signInWithRedirect で認証ページで飛ばします。ここで重要なのがもう一つ用意されている signInWithPopup は使わないことです。Firebase のドキュメントでは signInWithPopup で書かれているので、こちらのほうがデフォルトっぽい感じですが signInWithRedirect を使ったほうが絶対に良いです。

なぜなら signInWithPopup の場合、スマホアプリのアプリ内ブラウザ (WebView) だとポップアップのタブが正常に処理されずログインができない状況があるからです。Firebase のドキュメントにも モバイル端末ではリダイレクトすることをおすすめします。 と書かれていますが、今どきならモバイル端末を意識しないアプリケーションはないと思うので素直に signInWithRedirect を使っていきましょう。

その代わり? signInWithRedirect の方が認証の流れが分断されて面倒くさいですが…

<template>
  <div>
    <button @click="authGoogle">Google login</button>
  </div>
</template>

<script>
import * as firebase from 'firebase/app'
export default {
  name: 'Home',
  methods: {
    authGoogle () {
      const provider = new firebase.auth.GoogleAuthProvider()
      firebase.auth().signInWithRedirect(provider)
    }
  }
}
</script>

これで SPA での認証の仕組みは実装完了です。

SSR で認証

次は SSR での認証です。SSR で認証するということは一般的な Web アプリケーションと同じように Cookie などを使って認証する必要があります。しかし、Firebase Authentication の Web SDK ではクライアントから直接 JavaScript を用いて直接 Firebase の認証サーバへ情報を送信することになります。つまり SSR をしている Web サーバにも認証情報を送信する必要があります。

Firebase ドキュメントにセッション Cookie を使った管理方法が紹介されています。これを見ると Web SDK で認証後に getIdToken() を使って認証トークンを取得、SSR サーバに送信してセッション Cookie を発行するようにしています。これによって SSR サーバだけでクライアントが認証済みかそうでないかを判断することができます。

本来であれば上記のセッション Cookie を使って認証するのが王道なのですが、今回は SPA にも SSR にも対応可能な形で実装をしたかったので SPA で行ったようなクライアントだけで完結する方法で行います。

router/index.js

先程 SPA で Vue Router の Router.beforeEach を使って遷移前に認証状況を確認していましたが、 SSR をすると Vue Router はサーバ側の処理になるため onAuthStateChanged はここでは利用できません。正確に言うと初回アクセス時にはサーバ側の処理になって認証情報は利用できません。以降のページ遷移は SPA と同じくクライアントでの処理になるので利用できます。

Router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // すでに認証済みの場合は遷移
    if (firebase.auth().currentUser) {
      next()
      return
    }
    // まだ認証されていない場合は Firebase SDK からのコールバックを待つ…
    // けどサーバ側の処理なので何も手に入らない
    firebase.auth().onAuthStateChanged(user => {
      if (user) {
        next()
      } else {
        next({ name: 'Home' })
      }
      return
    })
  }
  next()
})

初回アクセス時に認証情報が利用できなくて、なぜ困るのかというと認証が必要なページへ直接アクセスされたときに制御する方法がないからです。認証が必要なページの beforeCreate のタイミングで認証確認をしたとしても、認証は非同期処理なため見えてはいけないページが見えてしまう可能性があります。

そこで Vue Router 側では Vuex の State を使って認証済みかどうかだけを判断させるようにします。そして認証されていない場合は認証専用ページへ飛ばします。

Router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (store.getters['auth/alreadyLoggedIn'] === true) {
      next()
      return
    }
    // Auth コンポーネントで Firebase 認証を行ってから目的先へリダイレクトさせる
    next({ name: 'Auth', query: { redirect: to.fullPath } })
  } else {
    next()
  }
})

認証用の Vuex ストアを用意します。今回は単純に Firebase Authentication の uid を保持するだけのものにします。実際には Firestore などのデータベースとの連携もあると思います。それらはここで合わせて実装するのが良いでしょう。

store/auth/state.js

export default {
  uid: null
}

store/auth/getters.js

export const alreadyLoggedIn = state => {
  return state.uid !== null && firebase.auth().currentUser !== null
}

store/auth/actions.js

export const login = async ({ commit }, uid) => {
  // Firestore などの処理があればここで
  commit('LOGIN', { uid: uid })
}

store/auth/mutations.js

export const LOGIN = (state, uid) => {
  state.uid = uid
}

pages/Auth.vue

認証するだけの Auth ページです。見た目は真っ白なので処理中というのをユーザに伝えるためのローディングのインジケータなどがあるよ良いと思います。Firebase Authentication の認証コールバックで認証が確認されたら目的のページへリダイレクトしてあげます。

<template>
  <div></div>
</template>

<style>
</style>

<script>
import { mapActions } from 'vuex'
export default {
  name: 'Auth',
  methods: {
    ...mapActions('auth', [
      'login'
    ])
  },
  beforeCreate () {
    // ここでローディングのインジケータアニメーションを表示すると良い
    firebase.auth().onAuthStateChanged(async user => {
      if (user) {
        await this.login(user.uid)
        this.$router.push(this.$route.query.redirect)
      } else {
        this.$router.push({ name: 'Home' })
      }
    })
  }
}
</script>

これで SSR しながらも SPA との共存が可能な認証の流れができました。サーバサイド側で認証の処理をしない分、Auth ページというワンクッションが挟まっていますが、どちらでも使えるというのが活きてくるシーンもあると思います。まぁ…王道的にはあまりないと思いますが…。

実際にはユーザ情報登録画面や Firestore との連携もあるのでもっと複雑な分岐にはなると思いますが、基本はこの考え方で行けると思います。実際のコードは GitHub にアップしているので参考にしてみてください。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48