Next.js 13.4.12 App Router下Dashboard路由保护及布局闪烁问题
Next.js 13.4.12 App Router + Firebase Auth 路由保护方案
1. 中间件实现路由保护(解决Edge runtime不兼容firebase-admin问题)
由于Next.js中间件运行在Edge runtime,无法使用依赖Node.js API的firebase-admin SDK,可改用Firebase Auth官方REST API验证令牌,实现类似next-auth的中间件路由拦截效果:
步骤1:编写中间件代码
// middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export async function middleware(request: NextRequest) { // 从Cookie中获取Firebase Auth的ID令牌(键名根据你的实际登录逻辑调整) const idToken = request.cookies.get('__session')?.value; // 无令牌直接重定向登录页 if (!idToken) { return NextResponse.redirect(new URL('/login', request.url)); } try { // 调用Firebase Auth REST API验证令牌有效性 const verifyRes = await fetch( `https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=${process.env.NEXT_PUBLIC_FIREBASE_API_KEY}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ idToken }), } ); const verifyData = await verifyRes.json(); // 令牌无效或验证失败,重定向登录页 if (!verifyRes.ok || !verifyData.users?.length) { return NextResponse.redirect(new URL('/login', request.url)); } // 验证通过,允许访问目标路由 return NextResponse.next(); } catch (err) { // 异常情况也重定向登录页 return NextResponse.redirect(new URL('/login', request.url)); } } // 匹配/dashboard下所有子路由 export const config = { matcher: '/dashboard/:path*', };
步骤2:登录时存储令牌到Cookie
登录成功后,将Firebase返回的ID令牌存入Cookie,确保中间件能读取到:
// 登录逻辑示例(客户端/服务端组件均可) import { signInWithEmailAndPassword } from 'firebase/auth'; import { auth } from '@/firebase/config'; import { cookies } from 'next/headers'; async function handleLogin(email: string, password: string) { const userCredential = await signInWithEmailAndPassword(auth, email, password); const idToken = await userCredential.user.getIdToken(); // 设置Cookie,生产环境开启secure和httpOnly增强安全性 cookies().set('__session', idToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 24 * 7, // 有效期7天 path: '/', }); }
2. 避免React Context方案的布局闪烁
客户端Context判断认证状态时,因客户端渲染时机问题容易出现布局闪烁,可通过服务端组件前置验证结合加载状态解决:
步骤1:服务端前置验证用户状态
编写服务端工具函数获取并验证用户:
// app/lib/getUser.ts import { cookies } from 'next/headers'; export async function getUser() { const idToken = cookies().get('__session')?.value; if (!idToken) return null; try { const verifyRes = await fetch( `https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=${process.env.NEXT_PUBLIC_FIREBASE_API_KEY}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ idToken }), } ); const verifyData = await verifyRes.json(); if (!verifyRes.ok || !verifyData.users?.length) return null; return verifyData.users[0]; } catch (err) { return null; } }
步骤2:在Dashboard布局中拦截未登录请求
将Dashboard的布局设为服务端组件,前置验证用户状态,未登录直接重定向:
// app/dashboard/layout.tsx import { redirect } from 'next/navigation'; import { getUser } from '@/app/lib/getUser'; import { AuthProvider } from '@/app/components/AuthProvider'; // 客户端Context组件 export default async function DashboardLayout({ children, }: { children: React.ReactNode; }) { const user = await getUser(); // 未登录直接重定向,不会渲染Dashboard布局 if (!user) { redirect('/login'); } // 把服务端获取的用户信息传给客户端Context,保持状态同步 return ( <AuthProvider initialUser={{ uid: user.localId, email: user.email }}> <div className="dashboard-container"> {/* Dashboard公共布局内容 */} {children} </div> </AuthProvider> ); }
步骤3:添加加载状态组件
在Dashboard路由下创建loading.tsx,服务端验证期间显示加载状态,避免空白或闪烁:
// app/dashboard/loading.tsx export default function DashboardLoading() { return ( <div className="flex justify-center items-center h-screen"> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div> </div> ); }
内容的提问来源于stack exchange,提问作者auto22
相关产品推荐
相关产品推荐

