この記事は「株式会社オープンストリーム Advent Calendar 2019」の19日目の記事です。
どうも、最近はFirebaseに触れていない @ysd_marrrr です。
今回はFirebaseのAuthenticationとFirestoreでユーザーを認証して、そのユーザーでやり取りするサンプルを作りました。
フロントエンドはNuxtを使っているのですが、Firebaseとどう連携させるか悩みながら決めたので共有します。
作るサンプルについて
「Twitterのタイムラインでは遥か彼方に流れてしまう、だけども自分のプロフィールにピン留めしておくものでもない情報をみんなでシェアする」伝言板を作ります。
- Twitterにログインしなくとも見ることはできます
- Twitterでログインすると書き込めるようになります
- 自分の書きこんだ投稿「だけ」削除できるものとします
ソースコードはこちらになります。
https://github.com/ysd-marrrr/nuxt-firebase-board
開発環境
macOSと
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.15.5
BuildVersion: 19F101
$ node -v
v14.5.0
$ npm -v
6.14.5
$ yarn -v
1.22.5
$ firebase -V
8.10.0
Windowsで動作確認をしています。
C:\Windows>ver
Microsoft Windows [Version 10.0.18363.476]
...
$ node -v
v13.2.0
$ npm -v
6.13.1
$ yarn -v
1.21.0
プロジェクトの作成
create-nuxt-app
でプロジェクトを作ります。FirebaseのHostingを使うため"Choose custom server framework"は None
にします(ローカルでフロントエンドの確認はできます!)
$ npx create-nuxt-app nuxt-firebase-board
create-nuxt-app v2.12.0
✨ Generating Nuxt.js project in nuxt-firebase-board
? Project name nuxt-firebase-board
? Project description A board sample with Nuxt.js + Firebase(Auth, Cloud Firestore)
? Author name ysd-marrrr
? Choose the package manager Yarn
? Choose UI framework Bulma
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Axios, Progressive Web App (PWA) Support, DotEnv
? Choose linting tools ESLint, Prettier, Lint staged files
? Choose test framework None
? Choose rendering mode Single Page App
? Choose development tools jsconfig.json (Recommended for VS Code)
UIの作成
Nuxt + Firebaseの組み合わせが本記事のテーマのため、ここでは省略します。サンプルコードを見てみてください🙄
Atomic Design もどきで 進めています 気力あったらUIパーツを分けます
https://github.com/ysd-marrrr/nuxt-firebase-board/tree/master/layouts
https://github.com/ysd-marrrr/nuxt-firebase-board/tree/master/pages
https://github.com/ysd-marrrr/nuxt-firebase-board/tree/master/components
Firebaseの導入
Firebaseのコンソールを開き、プロジェクトを追加します。Google アナリティクスの利用は任意です。
Firebaseのプロジェクトを作り終わると「開始するにはアプリを追加してください」と表示されるので、3番目の「ウェブ」をクリックします。
再び新規作成する名前を入力しますが、 プロジェクトの中に複数のアプリを追加できる仕組みのため わかりやすい名前にしましょう。
- 「Firebase Hostingを設定します」にチェックを入れます
- 次の「Firebase SDK の追加」はHTMLに直接挿入するためのオプションで、npmのパッケージからFirebaseを利用するため 何もせず次に進めます
開発環境からFirebaseのプロジェクトを操作する firebase-tools
を導入します。
npm install -g firebase-tools
次に、 create-nuxt-app
で作成したプロジェクトのルートディレクトリに移動して、Firebaseのプロジェクトを設定します。
ログインの画面が出たらFirebaseのプロジェクトを作ったGoogle アカウントでログインします。
firebase login
時の "? Allow Firebase to collect CLI usage and error reporting information?" は firebase-tools
を使っているときのエラー報告を送信するかどうかなので各自で Y/n を選んでください。
firebase login
firebase init
firebase init
の設定は次の通りにしました。 Spaceキーで複数選択してEnterキーで確定する項目 があるため「英語だから」と逃げずに質問文をよく読みましょう。
? Are you ready to proceed? Yes
? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. Hosting: Configure and deploy Firebase Hosting sites
=== Project Setup
? Please select an option: Use an existing project
? Select a default Firebase project for this directory: moments-sub (moments-sub)
i Using project nuxt-board-sample (nuxt-board-sample)
=== Hosting Setup
? What do you want to use as your public directory? public
? Configure as a single-page app (rewrite all urls to /index.html)? Yes
見づらいですが、序盤の「使用するサービス」を選択する質問で
- Hosting: Configure and deploy Firebase Hosting sites
を選択しています。
ここでFirestoreも選ぶと、のちにfirebase deploy
するときにHostingとFirestoreが同時にデプロイされてしまいます。慣れないうちはFirestoreを後で設定しましょう!
./public/index.html
を生成したと表示されますが、Nuxtでビルドしたものを使うため無視します。
Firebase Hostingを試す
プロジェクト設定時にHostingも設定しましたが、正しく設定されたかどうかNuxtでビルドしたものをHostingで確認してみましょう。
firebase init
のHostingの設定を変えてもよいのですが、Nuxtのビルド先を変更します。./nuxt.config.js
の generate.dir
を追加します。
buildDir
とこんがらがりますが、NuxtをSPAモードにしているためgenerate.dir
の設定が適用されます。
...
},
// 一番最後の"builds"の次に挿入する
generate: {
dir: 'public'
}
そして、ビルドした後にHostingにデプロイします。コンソールでデプロイされたサイトのURLが表示されるので確認してみてください!
yarn build
firebase deploy
確認が終わったらHostingを無効化します。大丈夫です、次回のfirebase deploy
時に復活します。
https://firebase.google.com/docs/cli?hl=ja#hosting-commands
firebase hosting:disable
Authentication の設定
Twitterアカウントでログインする…部分はFirebaseのAuthenticationで実装しましょう。
Twitterのアプリ登録
Firebase AuthenticationでTwitterのアカウント🐦を使って認証できるようにしましょう。
Twitterのアカウントを使った認証は、 Twitter側で開発者登録をして、その上で新しくアプリを作る必要があります。
登録方法は次の記事にまとまってますが、英語で開発者アカウント・アプリの用途を説明する必要があるため 面倒くさいですね そこは頑張って登録を進めましょう。
Twitter Developer 登録からアプリケーション作成まで〜 - Qiita
https://qiita.com/kei2ro/items/17fac4502119e9918763
アプリの作成ができると [Details] でアプリの詳細が開けるはずなので、そこの Keys and tokensでAPIキーを表示します。
使うAPIキーはConsumer API keysの方です。 Access token & access token secretは使いません。
2020/1/20からAPIキーの表示はアプリ作成時の一度きりになるそうです。アプリ作成時にAPIキーをメモできなかった場合はもう一度APIキーを生成(Regenerate)する必要があります。
https://twittercommunity.com/t/upcoming-changes-to-access-token-and-secret-management/130851
Firebase Authenticationの認証方法設定
そして、Firebase Authenticationのコンソールを開いて「ログイン方法を指定」をクリックします。
「ログインプロバイダ」としてログインに使える認証方法がたくさんありますが、その中の [Twitter] をクリックします。
設定が開くので、「有効にする」をクリックした後Twitterのアプリの詳細から APIキー
と APIシークレット
をコピーしましょう。
ここで「保存」を押してしまいそうですが、 「設定を完了するには、このコールバック URL を Twitter アプリの設定に追加します。」のURLをコピーします。
Twitterのアプリの詳細に戻り、App detailsから[Edit] -> [Edit details]をクリックして Callback URLs に貼り付けて保存しましょう。
また、[Enable Sign in with Twitter] にチェックを入れましょう。
Callback URLsの設定が終わったら、Firebase Authenticationのコンソールに戻ってこちらも保存しておしまいです。
Nuxtプロジェクト側にFirebaseを設定する
Nuxtプロジェクト側も設定します。先程Hostingは設定できましたが、NuxtのアプリからFirebase Authenticationを利用するにはnpmでFirebase SDKを導入する必要があります。
yarn add firebase
FirebaseのSDKが導入できたらFirebaseの接続情報を用意します。
コンソールでプロジェクトのトップを開き、作成したWebアプリの名前をクリックして歯車のアイコンをクリックします。
Webアプリの設定が開くので、 一番下の Firebase SDK Snippetまでスクロールして「構成」をクリックします。こちらが接続情報です。
そのFirebaseの接続情報を .env
ファイルに残します。create-nuxt-app
で追加した dotenv(./.env
) のファイルになります。
内容は次のように接続情報が並んでおり、 =
の後に設定値を書きます。文字列を表す ""
は不要です。
FB_API_KEY=AIza...
FB_AUTH_DOMAIN=nuxt-board-sample.firebaseapp.com
FB_DATABASE_URL=https://nuxt-board-sample.firebaseio.com
FB_PROJECTID=nuxt-board-sample
FB_STORAGE_BUCKET=nuxt-board-sample.appspot.com
FB_MESSAGING_SENDER_ID=354814904070
FB_APPID=1:354814904070:web:d93e...
FB_MESUREMENTID=G-6CYVTEMH8Q
この設定はWeb上に公開されるため、.gitignore
に登録して意図しない流出を防ぐ目的で dotenv
を使用しません(create-nuxt-app
でdotenv
を使うと自動的に.gitignore
に登録されます)
dotenv
は別のFirebaseプロジェクト/環境でアプリを使いまわす際に便利そうですね。
次に、./plugins/firebase-custom.js
を作成します。
NuxtでFirebaseを使えるうように初期化処理を追加しますが、 npm
で導入したFirebase SDKと混在するためplugins`のほうは名前を変えておきます。
import * as firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/firestore'
const firebaseConfig = {
apiKey: process.env.FB_API_KEY,
authDomain: process.env.FB_AUTH_DOMAIN,
databaseURL: process.env.FB_DATABASE_URL,
projectId: process.env.FB_PROJECTID,
storageBucket: process.env.FB_STORAGE_BUCKET,
messagingSenderId: process.env.FB_MESSAGING_SENDER_ID
appId: process.env.FB_APPID,
measurementId: process.env.FB_MEASUREMENTID
};
firebase.initializeApp(firebaseConfig)
const firestoreDb = firebase.firestore()
const dbSettings = { timestampsInSnapshots: true }
firestoreDb.settings(dbSettings)
// Firestoreの設定を適用するためfirebase.firestore()を読み替える
export { firestoreDb }
Firebaseの接続情報について
Firebaseのアクセスキーは、Javascript SDKの場合 公開される前提で設定します。 また、Firebase HostingではFirebaseの接続情報が公開されます。
そのため、誰でも(今作っているアプリをバイパスして)データベースであるCloud Firestoreにアクセスできる状態になります。
この時に 不正な操作からFirestoreのデータ(ドキュメント)を守るにはFirestoreのセキュリティルールのみが頼りになります。
こちらの記事で接続情報の公開について詳しく解説しています。
Firebase apiKey ってさらしていいの? ほんとに? - Qiita
https://qiita.com/hoshymo/items/e9c14ed157200b36eaa5
[WIP] Firebase Cloud Firestore 接続情報が漏れるとパケ死しないか気になっていたので調べてるメモ - かもメモ
https://chaika.hatenablog.com/entry/2019/01/22/133858
実際にアプリをリリースする前に不安な方はFirebase リリース チェックリスト を確認しましょう。
Authenticationの実装
一番悩みました はじめはVuexfireを使おうと考えましたが、ログインしたユーザーをどう結びつければよいのか、認証状態をどうやって管理するかサンプルをかなり調べてうんうん唸っていました。
さらには、AuthenticationでTwitterアカウントの認証を完了したとき、書き方によってはログインしてないとみなされる問題でハマりました。
そもそもVuexfireではAuthenticationのことは何も触れていません。
結論としては、こちらの例のように Vuexfireを使わずに Vuexを用いて
- stateでAuthenticationの認証状態を管理して
- actionでFirestoreの読み書き、更にはAuthenticationの認証状態を確認するなど…
をまとめたほうが、アプリケーション全体で認証情報にアクセスできて良さそうだと考えました(個人の考えです)
Nuxt.js と Firebase(Firestore)を使って認証と DB 保存を実装する - Qiita
https://qiita.com/ryamakuchi/items/ec71a20c45b32a382ef9
また、ログインする際にFirebase Authentication → Twitterのログイン画面 → 元のアプリの画面とリダイレクトさせる方法をお勧めしていますが、リダイレクトした時の認証情報の扱いもこちらのほうがすんなり対応できた気がします(要検証)
Twitter プロバイダ オブジェクトを使用して Firebase での認証を行います。ユーザーに Twitter アカウントでログインするよう促すために、ポップアップ ウィンドウを表示するか、ログインページにリダイレクトします。モバイル端末ではリダイレクトすることをおすすめします。
https://firebase.google.com/docs/auth/web/twitter-login?hl=ja
VuexストアにAuthenticationを実装する
Vuexストアである ./store/index.js
を作ります。
Authenticationを操作している部分は次のとおりです。
Changedとは言っていますが、認証状態を確認するために onAuthStateChanged
関数を呼ぶ必要があります。ログインするアクションを起こしていなくともログイン状態を確認するのであればこの関数が入ったactionを呼ぶことになります。 気持ち悪いと感じている人がいてよかった
...
export const actions = {
twitterSignIn({ dispatch }) {
firebase.auth().signInWithRedirect(new firebase.auth.TwitterAuthProvider())
dispatch('twitterAuthStateChanged')
},
twitterSignOut({ dispatch }) {
firebase.auth().signOut()
dispatch('twitterAuthStateChanged')
},
twitterAuthStateChanged({ dispatch, commit }) {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
const { displayName, uid } = user
commit('storeAuthInfo', {
userName: displayName,
firebaseUid: uid
})
} else {
commit('deleteAuthInfo')
}
})
},
...
- stateで「ログインしていない」初期値を定義して
- actionsでFirebase Authenticationの
firebase.auth().signInWithRedirect(new firebase.auth.TwitterAuthProvider())
を呼び出しログインさせ -
firebase.auth().onAuthStateChanged
の結果を見てstateを変更するMutationsを呼ぶ(commit)
Vuexストアにログイン情報を登録することで、こちらの例のようにNuxtのコンポーネントから $store.state
でログイン状態にアクセスできます。
<template>
...
<!--- ログインしていない状態 --->
<button
v-if="!$store.state.isLoggedin"
@click="signinWithTwitter"
class="button is-info is-large"
>
Twitterアカウントでログイン
</button>
<!--- ログインしている状態 --->
<div v-if="$store.state.isLoggedin">
ようこそ、 {{ $store.state.userName }}さん。
<button @click="signOut" class="button is-info is-large">
ログアウト
</button>
</div>
こちらのコンポーネントからVuexストアのActionsを呼び出すには、 @click
イベント -> methods
-> this.$store.dispatch()
で呼び出せます。
また、ページが読み込まれたとき(Vueがマウントされたとき)にもAuthenticationのログイン状態を確認します。
<script>
export default {
mounted() {
this.$store.dispatch('twitterAuthStateChanged')
},
methods: {
signinWithTwitter() {
this.$store.dispatch('twitterSignIn')
},
signOut() {
this.$store.dispatch('twitterSignOut')
}
}
}
</script>
実装できたら yarn dev
で試してみてください! ただし、ログインが完了してアプリに戻っても数秒間表示が変化しないことがあります。
Uncaught FirebaseError: projectId must be a string in FirebaseApp.options と表示される場合
yarn dev
したローカルのURLをブラウザーで開いても何も表示されず、コンソールを見てみると次のエラーで止まってしまうことがあります。
Uncaught FirebaseError: projectId must be a string in FirebaseApp.options
まずは.env
に記述してあるFirebaseの接続情報が正しく読み込めているかどうかを確認してください。また、.env
はデフォルトで.gitignore
に登録されているため、GitHubにpushして他のマシンでclone……なんてことをすると 認証情報がすっかり抜けてcloneされ、.env
を新規作成する必要があります。
コンソールでFirestoreをスタートさせる
コンソールからDatabaseを選び開始ボタンをクリックすると、はじめのセキュリティルールの設定が出ます。
セキュリティルールは テストモードで開始 を選択します。このルールを変更すると「Firebase Authenticationでログインしたユーザーだけ使える」ということができますが、初期の開発でFirestoreが使えると分かるまではテストモードにして「ルールが原因で動かない」ということを避けましょう。大丈夫です、後で直します
(Hostingを開始させてしまうと公開されるFirebaseの認証情報を悪用して、テストモードでガバガバなFirestoreに向けてガンガン攻撃されるかもしれないので、ルールをしっかりさせるまではHostingをやめておきましょう)
次に進んで質問されたFirestoreのリージョンは nam5(us-central)
を選びました。
Firestoreの作成が終わると、コレクションを追加する画面になります。Nuxt.jsからデータが取得できるかどうか確かめるために仮のデータをここで入れます。「+コレクションを開始」をクリックして board1
コレクションを作成します。
コレクションを作成するとドキュメントを追加する画面になるので、次のようにフィールドを設定します。値は自分で考えてください(適当)
ここではドキュメント IDは「自動ID」をクリックします。後でドキュメントIDにはAuthentication独自のユーザーIDを投入します。
Cloud Firestoreの実装
再びVuexストアに戻ります。Vuexストアの使い方はAuthenticationと似ていますが、
- Nuxtのコンポーネント側からVuexのactionをdispatchして
- VuexのactionでFirebaseを操作して
- 操作した結果をVuexのmutationにcommitで渡して
- VuexのmutationがVuexのstoreを変更して
- Nuxtのコンポーネントがそれを読み取り投稿した結果がユーザーに届く
実装になります。ここではFirebaseの操作部分について説明します。
すべてのユーザーの投稿を読み取る
こちらはログインしなくても見られるようにしたいので、Nuxtコンポーネントが読み込まれた(マウントされた)段階から実行します。
コンソールからFirestoreにデータを追加した際に コレクション ー ドキュメント ー フィールド
の構造になっていたのに気づいたかもしれません。コレクションに対して get()
をすると querySnapshot
の形でコレクション内の全ドキュメントが取得されます。
...
const recvMessages = []
firestoreDb
.collection('board1')
.get()
.then((querySnapshot) => {
// Firestoreからやってきたデータを扱いやすい形に変換する
querySnapshot.forEach(function(doc) {
recvMessages.push({ id: doc.id, data: doc.data() })
// 前に投稿したものがある場合は投稿フォームを隠す
if (doc.id === state.firebaseUid) {
commit('setPosted', true)
}
})
})
.catch((error) => {
console.error('Error getting document:', error)
})
.finally(function() {
// 成功した場合はメッセージのリストを、失敗したときは空のリストを使って表示させる
console.log(recvMessages)
commit('updateDisplayMessage', recvMessages)
})
...
投稿を追加・更新する
ドキュメントIDにはAuthenticationでユーザーごとに割り振られた uid
を用います。ドキュメントIDを指定するには doc()
を使います。
...
firestoreMessageAdd({ state, commit }, payload) {
const postedDate = new Date()
const postedTimestamp = Math.floor(postedDate.getTime() / 1000)
firestoreDb
.collection('board1')
.doc(state.firebaseUid)
.set({
userName: state.userName,
comment: payload.messageText,
date: postedTimestamp
})
.then(() => {
console.log('Document successfully written!')
commit('setPosted', true)
})
.catch((error) => {
console.error('Error writing document: ', error)
})
.finally(() => {
// 成功しようが失敗しようが最新の状態を取得する
this.dispatch('firestoreMessageCheck')
})
},
...
「新規登録は1度きり」を実現するために、今回は1度投稿すると新規投稿フォームはフロントエンド側で隠します(データベース側の制限は後ほどの「セキュリティルール」で設定します)
仮に同じIDを持つドキュメントに対してFirestoreの set()
を実行すると ドキュメント全体の上書きになります。 ドキュメントのフィールドだけを更新する場合は update()
を使います。
また、投稿が終わった際に最新の情報を取得するために再読込しています。大規模になってくると繰り返しコレクション全体を読み込むのは辛いので、最後に読み込んでから新しいものだけを追加する処理が必要になってきます。
投稿を削除する
コレクションとドキュメントIDを指定して delete()
を呼び出すだけです。 前は一旦検索してから削除したような気が
こちらも例によってアクション終了時に最新のメッセージを取得しています。
...
firestoreMessageDelete({ state, commit }) {
firestoreDb
.collection('board1')
.doc(state.firebaseUid)
.delete()
.then(function() {
console.log('Document successfully deleted!')
commit('setPosted', false)
})
.catch(function(error) {
console.error('Error removing document: ', error)
})
.finally(() => {
// 成功しようが失敗しようが最新の状態を取得する
this.dispatch('firestoreMessageCheck')
})
}
...
Firestoreのセキュリティルールを設定する
これで完成したように見えますがまだまだ足りません。公開するにはFirestoreの操作を制限しなければなりません。
Firebaseのコンソールを開いて[Database]->[ルール]を選択するとセキュリティルールを編集できます。
ログインしていないと書き込めないようにする
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read;
}
match /board1/{postID} {
allow create: if request.auth.uid != null;
}
}
}
何も設定しないと「拒否」になるため、セキュリティルールには許可する操作を記述します。
また、1ドキュメントに複数のセキュリティルールを設定すると OR で評価されます。
- はじめのワイルドカードで全員がドキュメントを見られるようにして
- ドキュメントを作成する
create
にはAuthenticationで認証されたIDがあるかどうかをチェックしています
match
で指定する場所には {}
で囲むと任意で変数を設定できます。ここではドキュメントのIDを変数にしています。
自分が投稿したドキュメント以外変更できないようにする
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read;
}
match /board1/{postID} {
allow write: if request.auth.uid == postID;
}
}
}
先程変数にしたドキュメントのIDとAuthenticationで認証したFirebase内部のIDをマッチさせて自分の投稿かどうかをチェックしています。
そしてルールに read
があるのなら write
もあるのでは?ルールで write
を指定すると create
, update
, delete
の操作が対象になります。
実はこの設定にすると「ログインしないと書き込めないようにする」も達成できます。ログインしていない状態では request.auth.uid
が null
になって一致しなくなるためです。今回はこの設定でおしまいです。
余裕があればセキュリティールールに反した書き込みを試してみて、Webブラウザの「コンソール」から書き込みが拒否されたときのメッセージを確認してみてください🙄
ルールシミュレータについて
左側にあるルールシミュレータでセキュリティルールを試すことができます。「許可されました」「拒否されました」と操作の結果が表示されるだけではなく、認証状態をセットしてシミュレーションもできるため便利です。
シミュレータに入力する「場所」には
- コレクションが
board
- ドキュメントIDが
documentid1
の場合 /board/documentid1
と指定します。 /databases/(default)/documents
が入力のサンプルだと勘違いして10分ほどハマりました。
Firestoreのドキュメントにあるサンプルの形でルールを設定してシミュレータで「認証なし」にすると「拒否されました」ではなく「Null value error」と表示されるのは 謎です (実際に書き込みを試してみるとMissing or insufficient permissions.になるため、正しく操作を制限できています)
おわりに
Twitterアカウントでログインして、一言残せるようなサービスを作ることができました。
Authenticationでログインしたときに認証状態が反映されるのに時間がかかるためその間の表示をどうするか定める必要がある、投稿が増えてきたときにFirestoreとのやり取りを減らす必要があるなど改善点はたくさんあります。
初めて触るとハマりどころが多いのですが、ユーザーの認証からデータのやり取りまで用意されていてしかも認証状態とデータを簡単につなげることができることができるのが魅力です。今回のアプリであればHostingでホストして公開まで持っていけます。
あれ、全投稿をVuexストアに詰めたら良くない気が…
参考
Nuxt.jsで手間取ったことまとめ - Qiita
https://qiita.com/ztrehagem/items/3c4accf04aa458b9f22e
Nuxt.js と Firebase(Firestore)を使って認証と DB 保存を実装する - Qiita
【改訂版】 Firebase Cloud Firestore rules tips
https://tech-blog.sgr-ksmt.org/2018/12/11/194022/
Firebase Cloud Firestoreのデータ更新 setとupdateの違い - ブロックチェーンエンジニアとして生きる
https://tomokazu-kozuma.com/difference-between-set-and-update-when-updating-cloud-firestore-data/