使用 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)
}
}最后更新于