FirebaseとSSRなNuxt.jsでアプリを作っていて、
クライアント側で認証チェックするとFirebaseの初期化などでラグが...
サーバ側で認証情報とかを取得してもう少しなんとかできないかなと。
まだベータっぽい?けど、公式の以下の内容を試してみたときの備忘録。
単語はよく聞くけど、ちゃんと見てなかったので、ざっくりとしたまとめ
$ npm install @nuxtjs/pwa
設定は簡単。modulesに@nuxtjs/pwa
を追加するだけ。
// nuxt.config.ts
import { Configuration } from "@nuxt/types";
const config: Configuration = {
// ...略
modules: [
// ... 略
"@nuxtjs/pwa",
],
// ...略
};
export default config;
このあたりを見つつ、Firebase Authの情報を扱えるようにする。
- Service Worker によるセッション管理 | Firebase
- Firebase (Hosting × Functions) × Nuxt.js (universal) で ユーザ認証のベストプラクティスを探る旅 その2 - Qiita
流れ的には、以下の通り。
長めだけど、ほぼ公式ドキュメントのまま。
// ~/static/sw-firebase-auth.js
// firebaseを初期化
firebase.initializeApp({
apiKey: /* API_KEY */,
authDomain: /* AUTH_DOMAIN */,
databaseURL: /* DATABASE_URL */,
projectId: /* PROJECT_ID */,
storageBucket: /* STORAGE_BUCKET */,
messagingSenderId: /* MESSAGING_SENDER_ID */,
appId: /* APP_ID */,
measurementId: /* AUTH_DOMAIN */
});
// onAuthStateChanged()で現在のuserからidTokenを取得
const getIdToken = () => {
return new Promise((resolve, reject) => {
const unsubscribe = firebase.auth().onAuthStateChanged(user => {
unsubscribe();
if (user) {
user.getIdToken().then(
idToken => resolve(idToken),
error => resolve(null)
);
} else {
resolve(null);
}
});
});
};
// URLからルートのURLを取得する処理
const getOriginFromUrl = url => {
// https://stackoverflow.com/questions/1420881/how-to-extract-base-url-from-a-string-in-javascript
const pathArray = url.split("/");
const protocol = pathArray[0];
const host = pathArray[2];
return protocol + "//" + host;
};
/**
* Service Workderのライフサイクルでfetchしたときの処理
*/
self.addEventListener("fetch", event => {
// リクエストをラップして、ヘッダにFirebase AuthのIdTokenを追加する処理
const requestProcessor = idToken => {
let req = event.request;
// URLを取得して、httpsもしくはlocalhostかなどをチェック
if (self.location.origin == getOriginFromUrl(event.request.url) &&
(self.location.protocol == "https:" || self.location.hostname == "localhost") &&
idToken
) {
// ヘッダ情報をクローンする
const headers = new Headers();
for (let entry of req.headers.entries()) {
headers.append(entry[0], entry[1]);
}
// クローンしたヘッダにFirebase AuthのIdTokenを追加
headers.append("Authorization", "Bearer " + idToken);
try {
req = new Request(req.url, {
method: req.method,
headers: headers,
mode: "same-origin",
credentials: req.credentials,
cache: req.cache,
redirect: req.redirect,
referrer: req.referrer,
body: req.body,
bodyUsed: req.bodyUsed,
context: req.context
});
} catch (e) {
console.error(e);
}
}
return fetch(req);
};
// 上の関数を使って、全リクエストでIdTokenの取得し、Firebase AuthのIdTokenを追加ようにする
event.respondWith(getIdToken().then(requestProcessor, requestProcessor));
});
/**
* Service Workderのライフサイクルでactivateしたときの処理
*/
self.addEventListener("activate", event => {
event.waitUntil(clients.claim());
});
やっていることは、以下のような感じ。
ヘッダーにIDトークンが付与されているので、
サーバ側でそれを見て、認証済みかをチェックする。
作成したサービスワーカを使うように、nuxt.config.tsに設定を追加する。
ドキュメントだとこのあたりを参照。
// nuxt.config.ts
import { Configuration } from "@nuxt/types";
const config: Configuration = {
// ...略
modules: [
// ... 略
"@nuxtjs/pwa",
],
workbox: {
// 追加するスクリプトを指定。
// バンドルされないので、CDNのfirebase-appを追加しておく。
importScripts: [
"https://www.gstatic.com/firebasejs/7.6.1/firebase-app.js",
"https://www.gstatic.com/firebasejs/7.6.1/firebase-auth.js",
"sw-firebase-auth.js"
],
// 開発中でもsw.jsが生成されるように設定。
dev: process.env.MODE != "production",
},
// ...略
};
export default config;
IDトークンもJWTデコードすると、UIDを取得できるけれど、
有効かどうかをfirebase-adminでチェックする必要がある。
なので、まずは、firebase-adminのインスタンスを初期化するファイルを用意。
// ~/utils/firebaseAdmin.ts
let admin;
if (process.server) {
admin = require("firebase-admin");
if (!admin.apps.length) {
const serviceAccount = require("./path/to/your/key.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://your-database-url.firebaseio.com"
});
}
}
export default admin;
次にこれを使って、以下をしていく。
以下は、vuex-module-decoratorsを使ったnuxtServerInitでチェックするサンプル。
// ~/store/index.ts
import { Context } from "@nuxt/types";
import { ActionContext } from "vuex/types";
import { ActionTree, Store } from "vuex";
import { initialiseStores } from "~/utils/store-accessor";
export const state = () => ({});
export type RootState = ReturnType<typeof state>;
const initializer = (store: Store<any>) => initialiseStores(store);
export const plugins = [initializer];
export const actions: ActionTree<any, any> = {
async nuxtServerInit(
context: ActionContext<RootState, RootState>,
server: Context
) {
// requestのAuthorizationからIDトークンを取得
const authorizationHeader = req.headers.authorization || "";
const components = authorizationHeader.split(" ");
const token = components.length > 1 ? components[1] : "";
if (!token) return;
// firebase-adminの初期化
const admin = require("~/utils/firebaseAdmin").default;
if (!admin) return;
// IDトークンの検証: 有効期限などをFirebaseでチェック
const decodedClaims = await admin.auth().verifyIdToken(token);
// 検証結果からUIDを取得
const uid = decodedClaims.uid;
// TODO: 認証状態に応じてなにかする
}
};
export * from "~/utils/store-accessor";
firebase-adminを取得する部分を
const admin = require("firebase-admin");
としていたり、if (process.server)
やif (!admin.apps.length)
などのチェックをせずにいたら、
クライアント側でもバンドルされていて、うまく動かいない状態に...
これで認証が必要なページでもいい感じSSRできた気がする(´ω`)
ただ、課題が残っていて、スーパーリロード/ハードリロードすると、
Service Workderを介して、リクエストされないので、ヘッダに認証情報が付与されない...
ログを見ていると、スーパーリロード時には、描画されたあとに、Service Workderの登録されているよう。
そういった場合でも、利用したい場合には、従来のセッションCookieを利用する方法がよいかも?
(もし良い方法があれば、教えてほしいです...)
ここに書いてあった。
- Service Worker の紹介 | Web Fundamentals | Google Developers
以上!!
積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました!
積読ハウマッチは、Nuxt.js+Firebaseで開発してます!
もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ
要望・感想・アドバイスなどあれば、
公式アカウント(@MemoryLoverz)や開発者(@kira_puka)まで♪
Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。
また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!
こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?
コメント