Next.js(TypeScript)中Firebase Auth与中间件结合实现权限控制的问题
Hey Sam,我完全懂你遇到的这个坑——Firebase Auth的currentUser在Next.js中间件里一直返回null,客户端的AuthContext又没法在服务器端的中间件里用,这确实是个常见的环境差异问题。让我一步步帮你解决:
为什么auth.currentUser在中间件里是null?
先搞清楚本质:currentUser是Firebase Auth客户端SDK维护的客户端状态,而Next.js中间件运行在服务器端,两者不在同一个环境,中间件根本没法直接访问客户端的Auth状态。我们得换个思路,把用户的认证信息从客户端传递到服务器端。
解决方案:用ID Token + HttpOnly Cookie做服务器端验证
核心逻辑是:客户端登录后,把Firebase的ID Token存入HttpOnly Cookie,中间件读取这个Cookie并验证Token有效性,以此判断用户是否有权访问受保护路由。
步骤1:客户端登录后将ID Token存入Cookie
在你的AuthContext或登录逻辑里,监听用户状态变化,获取ID Token后通过API路由存入HttpOnly Cookie(前端JS没法直接设置HttpOnly Cookie,必须通过服务器端)。
比如修改你的useAuth hook:
import { onAuthStateChanged, getAuth, getIdToken } from "firebase/auth"; import { useEffect } from "react"; export const useAuth = () => { const auth = getAuth(); useEffect(() => { const unsubscribe = onAuthStateChanged(auth, async (user) => { if (user) { // 获取用户的ID Token const idToken = await getIdToken(user); // 调用API路由设置Cookie await fetch("/api/set-firebase-token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token: idToken }), }); } else { // 用户登出时清除Cookie await fetch("/api/clear-firebase-token", { method: "POST" }); } }); return unsubscribe; }, [auth]); };
然后创建两个API路由处理Cookie的设置和清除:
/app/api/set-firebase-token/route.ts
import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; export async function POST(request: NextRequest) { const { token } = await request.json(); const response = NextResponse.json({ success: true }); // 设置安全的HttpOnly Cookie,防止XSS攻击 response.cookies.set("firebase-token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict", maxAge: 60 * 60 * 24, // 有效期1天 path: "/", }); return response; }
/app/api/clear-firebase-token/route.ts
import { NextResponse } from "next/server"; export async function POST() { const response = NextResponse.json({ success: true }); response.cookies.delete("firebase-token"); return response; }
步骤2:在中间件里验证Token并控制访问
现在修改你的中间件,读取Cookie里的Token,用Firebase Admin SDK验证其有效性(比客户端SDK更安全,无需暴露客户端API密钥):
首先安装Firebase Admin:
npm install firebase-admin
然后修改middleware.ts:
import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' import admin from 'firebase-admin'; // 确保Firebase Admin只初始化一次 if (!admin.apps.length) { admin.initializeApp({ credential: admin.credential.cert( JSON.parse(process.env.FIREBASE_ADMIN_SERVICE_ACCOUNT || '') ), }); } const protectedRoutes = ["/article/add"]; export async function middleware(request: NextRequest) { // 从Cookie中获取Token const token = request.cookies.get("firebase-token")?.value; let isAuthenticated = false; if (token) { try { // 验证ID Token的有效性 const decodedToken = await admin.auth().verifyIdToken(token); // decodedToken包含用户UID、邮箱、自定义声明等,可用于更细粒度的权限控制 console.log("当前认证用户UID:", decodedToken.uid); isAuthenticated = true; } catch (error) { // Token无效或过期,清除Cookie并跳转到登录页 console.error("Token验证失败:", error); const response = NextResponse.redirect(new URL("/login", request.nextUrl.origin)); response.cookies.delete("firebase-token"); return response; } } // 检查是否访问受保护路由且未认证 if (!isAuthenticated && protectedRoutes.includes(request.nextUrl.pathname)) { return NextResponse.redirect(new URL("/login", request.nextUrl.origin)); } // 允许正常访问 return NextResponse.next(); }
额外优化建议
- 细粒度权限控制:如果需要基于用户角色(如管理员/普通用户)控制路由,可以在Firebase控制台给用户添加自定义声明(比如
role: "admin"),然后在中间件里通过decodedToken.role判断权限。 - Token自动刷新:Firebase ID Token有效期为1小时,你可以在客户端监听Token过期事件,自动刷新Token并更新Cookie,避免用户频繁登录。
备注:内容来源于stack exchange,提问作者Sam B

