如何在Next.js 13(App目录)中处理服务端授权及角色路由保护?
Next.js 13 App目录架构下的服务端角色授权实现
一、核心逻辑概述
基于服务端HttpOnly Cookie存储的accessToken/refreshToken,在服务端完成令牌解析、用户身份校验及角色权限判断,通过服务端组件或中间件拦截未授权请求,实现路由级别的权限控制。
二、封装服务端工具函数
1. 解析并验证令牌
创建app/lib/auth.ts文件,封装服务端获取用户会话的核心逻辑:
import { cookies } from 'next/headers'; import jwt from 'jsonwebtoken'; // 定义用户信息类型 type User = { firstname: string; lastname: string; roles: string[]; }; type TokenPayload = { user: User; }; export async function getServerSession() { const cookieStore = cookies(); const accessToken = cookieStore.get('accessToken')?.value; if (!accessToken) { return null; } try { // 从环境变量读取JWT密钥,切勿硬编码 const decoded = jwt.verify(accessToken, process.env.JWT_SECRET!) as TokenPayload; return decoded.user; } catch (error) { // 处理令牌过期,调用刷新令牌逻辑 const refreshToken = cookieStore.get('refreshToken')?.value; if (refreshToken) { // 调用后端接口验证refreshToken并获取新accessToken const response = await fetch(`${process.env.API_URL}/refresh-token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken }), }); if (response.ok) { const { newAccessToken } = await response.json(); // 更新accessToken Cookie,配置安全属性 cookieStore.set('accessToken', newAccessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 60 * 15, // 15分钟有效期 path: '/', }); // 重新解析新令牌返回用户信息 const decoded = jwt.verify(newAccessToken, process.env.JWT_SECRET!) as TokenPayload; return decoded.user; } } return null; } } // 角色校验工具函数 export function hasRole(user: User | null, requiredRole: string) { return user?.roles.includes(requiredRole) ?? false; }
三、路由权限控制实现
1. 公开路由(如/login)
直接创建app/login/page.tsx,无需权限校验:
export default function LoginPage() { return ( <div> <h1>登录页面</h1> {/* 登录表单逻辑 */} </div> ); }
2. 需登录的路由(如/dashboard)
在app/dashboard/page.tsx中通过服务端组件校验会话:
import { getServerSession } from '@/app/lib/auth'; import { redirect } from 'next/navigation'; export default async function DashboardPage() { const user = await getServerSession(); if (!user) { // 未登录重定向至登录页 redirect('/login'); } return ( <div> <h1>欢迎 {user.firstname} 访问仪表盘</h1> {/* 仪表盘业务内容 */} </div> ); }
3. 需特定角色的路由(如/accounting)
在app/accounting/page.tsx中添加角色校验:
import { getServerSession, hasRole } from '@/app/lib/auth'; import { redirect } from 'next/navigation'; export default async function AccountingPage() { const user = await getServerSession(); if (!user || !hasRole(user, 'accounting')) { // 无权限重定向至登录页或返回403页面 redirect('/login'); // 或直接返回无权限提示:return <div>您无此模块访问权限</div> } return ( <div> <h1>会计管理模块</h1> {/* 会计业务内容 */} </div> ); }
4. 需admin角色的路由(如/admin)
在app/admin/page.tsx中实现管理员权限校验:
import { getServerSession, hasRole } from '@/app/lib/auth'; import { redirect } from 'next/navigation'; export default async function AdminPage() { const user = await getServerSession(); if (!user || !hasRole(user, 'admin')) { redirect('/login'); } return ( <div> <h1>管理员面板</h1> {/* 管理员专属内容 */} </div> ); }
5. 路由组全局校验(可选)
若某组路由需统一权限,可在layout.tsx中批量校验,例如创建app/(protected)/layout.tsx:
import { getServerSession } from '@/app/lib/auth'; import { redirect } from 'next/navigation'; export default async function ProtectedLayout({ children }: { children: React.ReactNode }) { const user = await getServerSession(); if (!user) { redirect('/login'); } return <div>{children}</div>; }
将/dashboard、/accounting等需登录的页面放入app/(protected)目录下,无需每个页面单独校验登录状态。
四、关键注意事项
- 所有权限校验必须在服务端组件或Middleware中完成,禁止在客户端暴露权限规则。
- JWT密钥、接口地址等敏感信息必须通过环境变量存储。
- 确保
accessToken和refreshToken的Cookie配置httpOnly、secure(生产环境)、sameSite: strict等安全属性。 - 无权限请求可根据业务需求返回403页面或重定向至登录页。
内容的提问来源于stack exchange,提问作者hantoren
相关产品推荐
相关产品推荐

