Next.js 14如何实现基于角色的路由保护及Refresh Token处理?
Next.js 14 基于角色的路由保护实现方案
一、基于角色的路由保护实现(SSR、CSR、Middleware)
1. Middleware 方式(全局拦截,推荐)
Middleware 是 Next.js 13+ 的全局路由拦截方案,能在请求到达页面之前完成权限校验,优先级最高,适合全局统一管控路由权限。
实现步骤与代码示例
在项目根目录创建 middleware.ts 文件:
import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' // 定义路由-角色映射规则 const ROLE_ROUTES = { '/admin': ['admin'], '/seller': ['seller'], '/buyer': ['buyer'], '/profile': ['admin', 'seller', 'buyer'] // 多角色可访问的公共路由 } export async function middleware(request: NextRequest) { const accessToken = request.cookies.get('accessToken')?.value let userRole = null if (accessToken) { try { // 调用API验证token并获取用户角色 const res = await fetch('http://localhost:5000/api/v1/verify-token', { headers: { Authorization: `Bearer ${accessToken}` }, credentials: 'include' }) if (res.ok) { const user = await res.json() userRole = user.role } else { // Token无效,清除Cookie并重定向登录页 const response = NextResponse.redirect(new URL('/login', request.url)) response.cookies.delete('accessToken') return response } } catch (err) { const response = NextResponse.redirect(new URL('/login', request.url)) response.cookies.delete('accessToken') return response } } // 匹配当前路由对应的权限要求 const currentPath = request.nextUrl.pathname const matchedRoute = Object.keys(ROLE_ROUTES).find(route => currentPath.startsWith(route)) const requiredRoles = matchedRoute ? ROLE_ROUTES[matchedRoute] : null if (requiredRoles) { // 无角色或角色不匹配时,重定向到登录页 if (!userRole || !requiredRoles.includes(userRole)) { return NextResponse.redirect(new URL('/login', request.url)) } } return NextResponse.next() } // 指定需要拦截的路由范围 export const config = { matcher: ['/admin/:path*', '/seller/:path*', '/buyer/:path*', '/profile'] }
2. SSR 方式(服务器端渲染时校验)
在服务器端获取 Cookie 并完成权限校验,适合需要在渲染前获取用户数据的页面,支持 Pages Router 和 App Router。
App Router 代码示例
// app/admin/page.tsx import { cookies } from 'next/headers' import { redirect } from 'next/navigation' async function AdminPage() { const cookieStore = cookies() const accessToken = cookieStore.get('accessToken')?.value // 无Token直接跳转登录页 if (!accessToken) redirect('/login') let userRole = null try { const res = await fetch('http://localhost:5000/api/v1/verify-token', { headers: { Authorization: `Bearer ${accessToken}` }, credentials: 'include' }) if (!res.ok) throw new Error('Token无效') const user = await res.json() userRole = user.role } catch (err) { redirect('/login') } // 角色不匹配跳转403页面 if (userRole !== 'admin') redirect('/403') return ( <div> <h1>Admin Dashboard</h1> {/* 页面内容 */} </div> ) } export default AdminPage
3. CSR 方式(客户端渲染时校验)
在客户端组件中通过钩子函数完成权限校验,适合纯客户端交互的页面。
自定义权限钩子示例
// hooks/useAuth.ts import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' export function useAuth(requiredRoles: string[]) { const [user, setUser] = useState<{ email: string; role: string } | null>(null) const [loading, setLoading] = useState(true) const router = useRouter() useEffect(() => { const checkAuth = async () => { // 从Cookie中提取accessToken const accessToken = document.cookie.split('; ') .find(row => row.startsWith('accessToken=')) ?.split('=')[1] if (!accessToken) { router.push('/login') setLoading(false) return } try { const res = await fetch('http://localhost:5000/api/v1/verify-token', { headers: { Authorization: `Bearer ${accessToken}` }, credentials: 'include' }) if (res.ok) { const userData = await res.json() if (!requiredRoles.includes(userData.role)) { router.push('/403') } else { setUser(userData) } } else { router.push('/login') } } catch (err) { router.push('/login') } finally { setLoading(false) } } checkAuth() }, [router, requiredRoles]) return { user, loading } }
页面使用示例
// app/seller/page.tsx 'use client' import { useAuth } from '@/hooks/useAuth' function SellerPage() { const { user, loading } = useAuth(['seller']) if (loading) return <div>加载中...</div> return ( <div> <h1>Seller Dashboard</h1> <p>欢迎,{user?.email}</p> </div> ) } export default SellerPage
二、结合Refresh Token的路由保护与令牌刷新
1. 刷新时机
- API请求返回401时:accessToken过期,先尝试刷新令牌,成功后重试原请求,失败则跳转登录页。
- 页面初始化校验时:SSR/CSR权限校验失败(401)时,先执行令牌刷新逻辑,再重新校验。
- 定时主动刷新:根据accessToken过期时间(如3分钟),在过期前30秒自动调用刷新接口(仅适合CSR场景)。
2. 实现方案
第一步:封装令牌刷新工具函数
// utils/auth.ts export async function refreshAccessToken(): Promise<boolean> { try { const res = await fetch('http://localhost:5000/api/v1/refresh-token', { method: 'POST', credentials: 'include' // 携带存储refreshToken的Cookie }) if (res.ok) { // 新的accessToken由API自动写入Cookie,无需手动处理 return true } else { // refreshToken也过期,清除所有授权Cookie document.cookie = 'accessToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' document.cookie = 'refreshToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' return false } } catch (err) { console.error('刷新令牌失败:', err) return false } }
第二步:在Middleware中集成刷新逻辑
修改 middleware.ts 中的Token校验逻辑:
// 省略其他代码... if (accessToken) { try { const res = await fetch('http://localhost:5000/api/v1/verify-token', { headers: { Authorization: `Bearer ${accessToken}` }, credentials: 'include' }) if (res.ok) { const user = await res.json() userRole = user.role } else if (res.status === 401) { // accessToken过期,尝试刷新令牌 const refreshSuccess = await refreshAccessToken() if (refreshSuccess) { // 刷新成功后,重新获取新Token并校验 const newAccessToken = request.cookies.get('accessToken')?.value if (newAccessToken) { const newRes = await fetch('http://localhost:5000/api/v1/verify-token', { headers: { Authorization: `Bearer ${newAccessToken}` }, credentials: 'include' }) if (newRes.ok) { const user = await newRes.json() userRole = user.role } else { throw new Error('新Token无效') } } else { throw new Error('获取新Token失败') } } else { throw new Error('刷新令牌失败') } } } catch (err) { const response = NextResponse.redirect(new URL('/login', request.url)) response.cookies.delete('accessToken') response.cookies.delete('refreshToken') return response } } // 省略其他代码...
第三步:在CSR钩子中集成刷新逻辑
修改 useAuth 钩子:
// hooks/useAuth.ts import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import { refreshAccessToken } from '@/utils/auth' export function useAuth(requiredRoles: string[]) { const [user, setUser] = useState<{ email: string; role: string } | null>(null) const [loading, setLoading] = useState(true) const router = useRouter() useEffect(() => { const checkAuth = async () => { let accessToken = document.cookie.split('; ') .find(row => row.startsWith('accessToken=')) ?.split('=')[1] if (!accessToken) { router.push('/login') setLoading(false) return } try { const res = await fetch('http://localhost:5000/api/v1/verify-token', { headers: { Authorization: `Bearer ${accessToken}` }, credentials: 'include' }) if (res.ok) { const userData = await res.json() if (!requiredRoles.includes(userData.role)) { router.push('/403') } else { setUser(userData) } } else if (res.status === 401) { // 尝试刷新令牌 const refreshSuccess = await refreshAccessToken() if (refreshSuccess) { // 刷新成功后重新校验 accessToken = document.cookie.split('; ') .find(row => row.startsWith('accessToken=')) ?.split('=')[1] const newRes = await fetch('http://localhost:5000/api/v1/verify-token', { headers: { Authorization: `Bearer ${accessToken}` }, credentials: 'include' }) if (newRes.ok) { const userData = await newRes.json() if (!requiredRoles.includes(userData.role)) { router.push('/403') } else { setUser(userData) } } else { router.push('/login') } } else { router.push('/login') } } else { router.push('/login') } } catch (err) { router.push('/login') } finally { setLoading(false) } } checkAuth() }, [router, requiredRoles]) return { user, loading } }
3. 注意事项
- 生产环境中,refreshToken的Cookie需设置
HttpOnly、Secure、SameSite: strict,防范XSS和CSRF攻击。 - 刷新令牌接口需校验refreshToken的有效性,返回新的accessToken并更新对应Cookie。
- 配置API的CORS规则,允许Next.js域名的请求携带Cookie。
内容的提问来源于stack exchange,提问作者Sakilahmed saju
相关产品推荐
相关产品推荐

