最近初めて、Firestoreを使ってwebアプリを作りました。
(諸事情で公開することができません。。。)
その時に僕が詰まったポイントを書いていきたいと思います。
構成
フロント:nuxt.js
バックエンド処理: cloud function
データーベース:firestore
認証周り:firebase authentication
サーバー: google App Engine
最近流行りの構成の乗っかりました。
1.JavaScript SDKとAdmin SDKは別物
これを理解しておらず、相当詰まりました。
(JavaScript SDKをimportしてAdmin SDKのAPIを呼んだりしてました)
JavaScript SDKはフロントで使い、
Admin SDKはcloud functionで使う
みたいな認識で良いと思います。
こちらの公式ドキュメントがとても参考になります。
https://firebase.google.com/docs/reference
SSR時にfirebase Authentication SDKの認証情報は参照できない。
Firebase Authentication SDKというログイン機能をよしなにしてくれるAPIがあるのですが、
こちらSSR時にその認証情報を参照することができないようです。
なので、SSR時にユーザーのログイン状態を取得して、処理を振り分けたい時には
初期ログイン時にユーザのauth情報をtoken化してcookieに
埋めておいたり、serviceworkerで持っておくようにします。
参考:https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl=ja
(cookieはセキュリティ的になんか怖いので僕はserviceWorkerを利用する方式にしました。)
セキュリティルールの無視と適用
Firestoreはバックエンドを介さずにフロントエンドから直接データーベースへの読み書きを可能にします。
その際にバックエンドバリデーションの役割を果たすのがセキュリティルールです。
例えばフロント側からやってくるデータに対して、以下のような基本的なバリデーションがかけれます。
(例:auth情報で弾く、文字数制限、正規表現で弾く)
そして大事なことがadmin SDKのAPIを利用した場合セキュリティルールが無視されることです。
注: Cloud Firestore セキュリティ ルールを使用するには、Cloud Firestore 用のモバイル ライブラリまたはウェブ クライアント ライブラリを使用する必要があります。サーバー クライアント ライブラリ、REST API または RPC API を使用すると、このルールは無視されます。
これを利用すると
特定のkeyに対してユーザー側からの書き込みは禁止する(ただし読み取りはさせる)
みたいなルールが書けます。
具体的には
改変不可能なユーザーが記事を作成した日付を作成したい
などの状況で使います。
具体的には初回書き込み時には日付をserverTimeStampで取得し、
cloud function経由でAdmin SDKを利用し書き込むことで、セキュリティルールを無視して書き込めます。
そしてセキュリティルールでドキュメントの更新、作成を禁止すれば後から改変できないkeyが作れます。
(cloudfunction側には日付のkeyが存在すればreturnするなどで2回目以降は処理を走らせないようにします。)
基本的なバリデーションはもちろん、このようなセキュリティルールの無視と適用
をうまく使うことが、セキュリティの設計に必要かと思いました。
セキュリティルールが配列に微妙に対応していない。
アプリにタグ機能を実装しようとした際に
配列形式でドキュメントに保存しようとしたのですが、
配列には厳密にセキュリティルールを適用することができず、詰まりました。
具体的には配列のバリデーションで
「ユーザーが入力した任意の値に対してバリデーションをかけること」ができなさそうでした。
(size()が使えない。。。。)
なので、Firestoreのサブコレクションを利用して
article
├ hogeArticleId
│ └ tag(サブコレクション)
│ └ hogetagName(ドキュメント)
└ hogetagName2(ドキュメント)
みたいな感じにすることにしました。
この構成だと読み取り時にサブコレクションまで潜る必要があるので、
そうしたくない場合は
hogeArticleaIdの階層に配列の形に整形して自動コピーするようにすると良いかもしれません。
(自動コピーは後述するFirestoreトリガーを利用します)
(これからFirestoreのアップデートで配列の個別要素に厳密なバリデーションがかけれるようになる可能性もあります。)
Firestoreに読み書きしたい時に、httpリクエストでcloud functionを叩くのはイケていない。
https://firebase.google.com/docs/functions/http-events?hl=ja
こちらの公式ドキュメントを見ると,フロントエンドからhttpリクエストを送り、
cloud functuonを叩いてfirestoreに対して、データ保存、取得ができるようなことを書いてありますが
、これはあまり使わない方がいいと思います。
なぜか
フロントから直接firestoreに対して読み書きした場合の方が早い。
一回cloud functionを叩く費用 > フロントエンドから直接Firestoreを操作する費用
cloud functionを挟むのでその分費用がかかる。corsに引っかかるのがしんどい。<- 個人的にこれが一番の理由。
などの理由から時に理由がない限りは、
Firestoreを読み書きするときは直接フロントエンドから呼び出すので良いと思います。
おそらくhttpリクエストを叩いてFirestoreを書き込み処理をしようとするのは
は書き込む際にデータのバリデーションをかけたい〜という背景があると思いますが、
Firestoreでは上記で書いたようなセキュリティールールにバリデーションを一任するのがスマートなやり方のようです。
複数のデータベースに書き込むときはFirestoreトリガーを使う。
フロントエンドはユーザ間で環境差が大きく、不安定です。
なので、複数のドキュメントに対して一括で書き込もうとした際に中途半端な書き込みになってしまう可能性があります。
例えば以下のように、
ユーザーの名前を変更した際に、そのユーザーが投稿した記事の著者名を変更する、みたいな場合に
ユーザーの名前の変更だけが行われた後、通信環境悪化などが起こった際に、著者名の変更だけが行われない
みたいなことが起こると悲しいですよね。
それを防ぐ方法として
- batch処理(書き込みを一括にまとめ、全て成功するか、失敗するかにする。)
- cloud functionでFirestoreトリガーを使う。
がありますが、僕がオススメしたいのは2の方法です。
Firestoreトリガーとはある特定のドキュメントへの書き込みを検知して、処理を走らせることができる機能です。
例えば
userコレクションに対してユーザー個別のドキュメントがあり、
またarticleコレクションの中に記事のドキュメントがあるとします。
そして記事のドキュメントは著者情報として、記事投稿者の名前を持っているとします。
user
├ hogeuserID
│ └ userName
│ └ uid
article
├ hogeArticleId
│ └ authorName
└
このような構成の時に、userドキュメントのuserName
を変更した時にarticleドキュメントのauthorName
も変更したいですよね。
そこでuserドキュメントの変更をトリガーとして、
articleドキュメントへの著者名の変更処理を別途走らせることができるのが
Firestoreトリガーです。
フロント側でbatch処理を使って複数のドキュメントに書き込むこともできますが、
なんか処理的にも重そうな感じがしますし、articleコレクションだけでなく、
commentドキュメントなんかもあった場合には3つのコレクションにフロントから同時に読み書きすることになり、
管理が煩雑になってきます。
Firestoreトリガーを利用する方が
情報管理を一本化できる(ユーザ情報を更新するだけでそれに付随するドキュメントの情報がFirestoreトリガーによって反映される、みたいにできる。変更のする際にユーザ情報さえ変えておけば良い、という風にできる。)
ので、僕はこちらの方が好きです。
終わり。
複雑なクエリ検索を使わないアプリであればFirestoreで一般公開できるものは作れそうだなと
思いました。