Skip to Content
LaunchExt | Chrome 扩展开发平台 (Next.js + Plasmo) 🚀 Read more → 
文档Firebase 认证集成指南

使用 Firebase 认证快速开始

由于这涉及的内容较多,我们写了一篇博客文章 ,详细逐步介绍了使 Firebase 认证工作所需的一切操作。

如需快速入门分解,请继续阅读:

概述

对于 Web 开发人员来说,浏览器扩展中的认证可能需要一些调整,但比直接在 React 或 Next.js 中使用 Firebase 并不困难多少。

我们基本上希望将令牌存储在 Cookie 中,以便后台服务工作者可以使用它们。

设置

首先,让我们安装一些依赖项。在 plasmo 中使用 Firebase 和操作 cookie 需要三个包。

npm install firebase @plasmohq/messaging @plasmohq/storage

或者

yarn add firebase @plasmohq/messaging @plasmohq/storage

或者

pnpm install firebase @plasmohq/messaging @plasmohq/storage

初始化 Firebase

首先,我们可以创建 Firebase 客户端应用程序的单例实例。

// src/firebase/firebaseClient.ts import { getApps, initializeApp } from "firebase/app" import { GoogleAuthProvider, getAuth } from "firebase/auth" import { getFirestore } from "firebase/firestore" import { getStorage } from "firebase/storage" const clientCredentials = { apiKey: process.env.PLASMO_PUBLIC_FIREBASE_PUBLIC_API_KEY, authDomain: process.env.PLASMO_PUBLIC_FIREBASE_AUTH_DOMAIN, projectId: process.env.PLASMO_PUBLIC_FIREBASE_PROJECT_ID, storageBucket: process.env.PLASMO_PUBLIC_FIREBASE_STORAGE_BUCKET, messagingSenderId: process.env.PLASMO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, appId: process.env.PLASMO_PUBLIC_FIREBASE_APP_ID, measurementId: process.env.PLASMO_PUBLIC_FIREBASE_MEASUREMENT_ID } let firebase_app // 检查 Firebase 应用是否已初始化,避免在热重载时创建新应用 if (!getApps().length) { firebase_app = initializeApp(clientCredentials) } else { firebase_app = getApps()[0] } export const storage = getStorage(firebase_app) export const auth = getAuth(firebase_app) export const db = getFirestore(firebase_app) export const googleAuth = new GoogleAuthProvider() export default firebase_app
👀

注意:您需要在 .env 文件中填充您的 Firebase 配置,这可以在您的 Firebase 控制台 中找到

PLASMO_PUBLIC_FIREBASE_CLIENT_ID="" PLASMO_PUBLIC_FIREBASE_PUBLIC_API_KEY="" PLASMO_PUBLIC_FIREBASE_AUTH_DOMAIN="" PLASMO_PUBLIC_FIREBASE_PROJECT_ID="" PLASMO_PUBLIC_FIREBASE_STORAGE_BUCKET="" PLASMO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="" PLASMO_PUBLIC_FIREBASE_APP_ID="" PLASMO_PUBLIC_FIREBASE_MEASUREMENT_ID=""

处理令牌

现在处理好了,我们可以创建一个可重用的钩子来提供登录和注销方法,以及在标签页/弹出窗口/选项页中轻松访问用户信息的方式

// src/firebase/useFirebaseUser.tsx import { type User, browserLocalPersistence, onAuthStateChanged, setPersistence } from "firebase/auth" import { useEffect, useState } from "react" import { sendToBackground } from "@plasmohq/messaging" import { auth } from "./firebaseClient" setPersistence(auth, browserLocalPersistence) export default function useFirebaseUser() { const [isLoading, setIsLoading] = useState(false) const [user, setUser] = useState<User>(null) const onLogout = async () => { setIsLoading(true) if (user) { await auth.signOut() await sendToBackground({ name: "removeAuth", body: {} }) } } const onLogin = () => { if (!user) return const uid = user.uid // 获取当前用户认证令牌 user.getIdToken(true).then(async (token) => { // 发送令牌到后台保存 await sendToBackground({ name: "saveAuth", body: { token, uid, refreshToken: user.refreshToken } }) }) } useEffect(() => { onAuthStateChanged(auth, (user) => { setIsLoading(false) setUser(user) }) }, []) useEffect(() => { if (user) { onLogin() } }, [user]) return { isLoading, user, onLogin, onLogout } }

您会注意到在上面的代码片段中我们调用了两个后台进程,removeAuth 和 saveAuth。现在让我们创建它们

// background/messages/saveAuth.ts import type { PlasmoMessaging } from "@plasmohq/messaging" import { Storage } from "@plasmohq/storage" const handler: PlasmoMessaging.MessageHandler = async (req, res) => { try { const { token, uid, refreshToken } = req.body const storage = new Storage() await storage.set("firebaseToken", token) await storage.set("firebaseUid", uid) await storage.set("firebaseRefreshToken", refreshToken) res.send({ status: "success" }) } catch (err) { console.log("出现错误") console.error(err) res.send({ err }) } } export default handler
// background/messages/removeAuth.ts import type { PlasmoMessaging } from "@plasmohq/messaging" import { Storage } from "@plasmohq/storage" const handler: PlasmoMessaging.MessageHandler = async (req, res) => { try { const storage = new Storage() await storage.set("firebaseToken", null) await storage.set("firebaseUid", null) res.send({ status: "success" }) } catch (err) { console.log("出现错误") console.error(err) res.send({ err }) } } export default handler

示例用法 后台服务工作者

很好,现在您将刷新令牌、ID 令牌和 UID 保存在 cookie 中。您可以在其他后台工作者中使用它们,如下所示

// 示例后台服务工作者,从集合 /users/{uid} 获取当前登录用户的数据 import type { PlasmoMessaging } from "@plasmohq/messaging" import { Storage } from "@plasmohq/storage" import refreshFirebaseToken from "~util/refreshFirebaseToken" const fetchUserData = async ( token, uid, refreshToken, storage, retry = true ) => { const response = await fetch( "https://firestore.googleapis.com/v1/projects/<firebase_project_id>/databases/(default)/documents/users/" + uid, { headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" } } ) const responseData = await response.json() if (responseData?.error?.code === 401 && retry) { const refreshData = await refreshFirebaseToken(refreshToken) await storage.set("firebaseToken", refreshData.id_token) await storage.set("firebaseRefreshToken", refreshData.refresh_token) await storage.set("firebaseUid", refreshData.user_id) // 刷新令牌后重试请求 return fetchUserData( refreshData.id_token, uid, refreshData.refresh_token, storage, false ) } return responseData } const handler: PlasmoMessaging.MessageHandler = async (req, res) => { try { const storage = new Storage() const token = await storage.get("firebaseToken") const uid = await storage.get("firebaseUid") const refreshToken = await storage.get("firebaseRefreshToken") const userData = await fetchUserData(token, uid, refreshToken, storage) res.send({ status: "success", data: userData }) } catch (err) { console.log("出现错误") console.error(err) res.send({ err }) } } export default handler

在上面的示例中,如果响应失败,我们会刷新令牌并重试。这是刷新 Firebase 令牌的实用方法。

type RefreshTokenResponse = { expires_in: string token_type: string refresh_token: string id_token: string user_id: string project_id: string } export default async function refreshFirebaseToken( refreshToken: string ): Promise<RefreshTokenResponse> { const response = await fetch( "https://securetoken.googleapis.com/v1/token?key=<firebase_api_key>", { method: "POST", body: JSON.stringify({ grant_type: "refresh_token", refresh_token: refreshToken }) } ) const { expires_in, token_type, refresh_token, id_token, user_id, project_id } = await response.json() return { expires_in, token_type, refresh_token, id_token, user_id, project_id } }

示例用法 React 钩子

您也可以在 React 组件中使用它(CSUI 或标签页/扩展页面)

这是使用 TailwindCSS 的 options.tsx 的示例登录页面设置。

// src/options.tsx import AuthForm from "components/AuthForm" import "./style.css" import useFirebaseUser from "~firebase/useFirebaseUser" export default function Options() { const { user, isLoading, onLogin } = useFirebaseUser() return ( <div className="min-h-screen bg-black p-4 md:p-10"> <div className="text-white flex flex-col space-y-10 items-center justify-center"> {!user && <AuthForm />} {user && <div>您已登录!哇</div>} </div> </div> ) }
// src/components/AuthForm.tsx import { createUserWithEmailAndPassword, signInWithEmailAndPassword } from "firebase/auth" import React, { useState } from "react" import { auth } from "~firebase/firebaseClient" import useFirebaseUser from "~firebase/useFirebaseUser" export default function AuthForm() { const [showLogin, setShowLogin] = useState(true) const [email, setEmail] = useState("") const [confirmPassword, setConfirmPassword] = useState("") const [password, setPassword] = useState("") const { isLoading, onLogin } = useFirebaseUser() const signIn = async (e: any) => { if (!email || !password) return console.log("请输入邮箱和密码") e.preventDefault() try { await signInWithEmailAndPassword(auth, email, password) } catch (error: any) { console.log(error.message) } finally { setEmail("") setPassword("") setConfirmPassword("") onLogin() } } const signUp = async (e: any) => { try { if (!email || !password || !confirmPassword) return console.log("请输入邮箱和密码") if (password !== confirmPassword) return console.log("密码不匹配") e.preventDefault() const user = await createUserWithEmailAndPassword(auth, email, password) onLogin() } catch (error: any) { console.log(error.message) } finally { setEmail("") setPassword("") setConfirmPassword("") } } return ( <div className="flex items-center justify-center w-full p-4 overflow-x-hidden rounded-xl overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full"> <div className="w-full max-w-2xl max-h-full"> <div className="p-10 bg-white rounded-lg shadow"> <div className="flex flex-row items-center justify-center"> {!isLoading && ( <span className="text-black font-bold text-3xl text-center"> {showLogin ? "登录" : "注册"} </span> )} {isLoading && ( <span className="text-black font-bold text-3xl text-center"> 加载中... </span> )} </div> <div className="p-4 rounded-xl bg-white text-black"> {!showLogin && !isLoading && ( <form className="space-y-4 md:space-y-6" onSubmit={signUp}> <div> <label htmlFor="email" className="block mb-2 text-sm font-medium text-gray-900"> 您的邮箱 </label> <input type="email" name="email" id="email" onChange={(e) => setEmail(e.target.value)} value={email} className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5" placeholder="name@company.com" required /> </div> <div> <label htmlFor="password" className="block mb-2 text-sm font-medium text-gray-900"> 密码 </label> <input type="password" name="password" id="password" onChange={(e) => setPassword(e.target.value)} value={password} placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5" required /> </div> <div> <label htmlFor="confirm-password" className="block mb-2 text-sm font-medium text-gray-900"> 确认密码 </label> <input type="password" name="confirm-password" id="confirm-password" onChange={(e) => setConfirmPassword(e.target.value)} value={confirmPassword} placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5" required /> </div> <div className="flex items-start"> <div className="flex items-center h-5"> <input id="terms" aria-describedby="terms" type="checkbox" className="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300" required /> </div> <div className="ml-3 text-sm"> <label htmlFor="terms" className="font-light text-gray-500"> 我接受{" "} <a className="font-medium text-primary-600 hover:underline" href="#"> 条款和条件 </a> </label> </div> </div> <button type="submit" className="w-full text-black bg-gray-300 hover:bg-primary-dark focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center"> 创建账户 </button> <p className="text-sm font-light text-gray-500"> 已有账户?{" "} <button onClick={() => setShowLogin(true)} className="bg-transparent font-medium text-primary-600 hover:underline"> 点击这里登录 </button> </p> </form> )} {showLogin && !isLoading && ( <form className="space-y-4 md:space-y-6" onSubmit={signIn}> <div> <label htmlFor="email" className="block mb-2 text-sm font-medium text-gray-900"> 您的邮箱 </label> <input type="email" name="email" id="email" onChange={(e) => setEmail(e.target.value)} value={email} className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5" placeholder="name@company.com" required /> </div> <div> <label htmlFor="password" className="block mb-2 text-sm font-medium text-gray-900"> 密码 </label> <input type="password" name="password" id="password" onChange={(e) => setPassword(e.target.value)} value={password} placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5" required /> </div> <div className="flex items-center justify-between"> <div className="flex items-start"> <div className="flex items-center h-5"> <input id="remember" aria-describedby="remember" type="checkbox" className="w-4 h-4 border border-gray-300 rounded accent-primary bg-gray-50 focus:ring-3 focus:ring-primary" /> </div> <div className="ml-3 text-sm"> <label htmlFor="remember" className="text-gray-500 "> 记住我 </label> </div> </div> <a href="#" className="text-sm font-medium text-primary-600 hover:underline"> 忘记密码? </a> </div> <button type="submit" className="w-full text-black bg-gray-300 hover:bg-primary-dark focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center"> 登录 </button> <p className="text-sm font-light text-gray-500"> 还没有账户?{" "} <button onClick={() => setShowLogin(false)} className="bg-transparent font-medium text-primary-600 hover:underline"> 注册 </button> </p> </form> )} </div> </div> </div> </div> ) }

注销方法可能如下所示

const logout = async (e: any) => { e.preventDefault() try { await onLogout() } catch (error: any) { console.log(error.message) } }
最后更新于