如何基于NestJS Cookie会话在Next.js 14实现角色访问控制
NestJS + Next.js 14(App Router)基于角色的访问控制实现方案
我正在开发一个以NestJS为后端、Next.js 14(App Router)为前端的项目。后端采用基于Cookie的会话认证,会话ID存储在Cookie中。需要借助NestJS后端的会话验证,在Next.js应用中为受保护路由设置基于角色的访问控制。
期望实现流程
- 用户向Next.js服务器发起请求。
- Next.js服务器从请求中提取会话Cookie,并转发至NestJS后端进行会话验证。
- NestJS后端验证会话,若有效则返回包含用户角色(如
adminId、studentId、teacherId)的会话数据。 - 根据后端响应处理:若返回401,Next.js服务器将用户重定向至
/login;若返回200,则检查用户角色与请求路由(/admin、/student、/teacher)是否匹配,仅当会话数据中存在对应角色ID时才提供该路由服务。
当前已使用next-intl实现国际化,现有中间件配置和区域验证代码如下:
原国际化中间件(middleware.ts)
import createMiddleware from "next-intl/middleware"; export default createMiddleware({ locales: ["en", "uk"], defaultLocale: "en", }); export const config = { matcher: ["/", "/(uk|en)/:path*"], };
原区域验证配置(i18n.ts)
import { notFound } from "next/navigation"; import { getRequestConfig } from "next-intl/server"; const locales = ["en", "uk"]; export default getRequestConfig(async ({ locale }) => { if (!locales.includes(locale as any)) notFound(); return { messages: (await import(`../messages/${locale}.json`)).default, }; });
实现方案
1. 整合国际化与权限验证的中间件改造
Next.js中间件仅支持导出一个默认函数,因此需要将next-intl的国际化逻辑与自定义权限验证逻辑合并:
import createMiddleware from "next-intl/middleware"; import { NextRequest, NextResponse } from "next/server"; // 初始化国际化中间件 const intlMiddleware = createMiddleware({ locales: ["en", "uk"], defaultLocale: "en", }); // 定义受保护路由与对应角色的映射关系 const protectedRoutes = { "/admin": "adminId", "/student": "studentId", "/teacher": "teacherId", }; // NestJS后端会话验证接口地址 const AUTH_VALIDATE_URL = "http://your-nestjs-backend/api/auth/validate-session"; export default async function middleware(request: NextRequest) { // 先执行国际化中间件逻辑,处理区域设置 const intlResponse = intlMiddleware(request); if (intlResponse) { return intlResponse; } // 去除区域前缀,获取纯净请求路径 const pathname = request.nextUrl.pathname; const cleanedPath = pathname.replace(/^\/(en|uk)/, "") || "/"; // 判断当前路径是否需要权限验证 const requiredRoute = Object.keys(protectedRoutes).find(route => cleanedPath.startsWith(route) ); if (!requiredRoute) { // 非受保护路由,直接放行 return NextResponse.next(); } try { // 提取请求Cookie,转发至NestJS后端验证会话 const cookies = request.headers.get("cookie"); const response = await fetch(AUTH_VALIDATE_URL, { method: "GET", headers: { Cookie: cookies || "", "Content-Type": "application/json", }, credentials: "include", }); if (response.status === 401) { // 会话无效,重定向至登录页并保存原请求路径 const loginUrl = new URL("/login", request.nextUrl.origin); loginUrl.searchParams.set("redirect", pathname); return NextResponse.redirect(loginUrl); } if (response.status === 200) { const sessionData = await response.json(); const requiredRoleKey = protectedRoutes[requiredRoute as keyof typeof protectedRoutes]; // 检查会话数据中是否包含对应角色ID if (!sessionData[requiredRoleKey]) { // 角色不匹配,返回403禁止访问 return new NextResponse("无访问权限", { status: 403 }); } } // 验证通过,放行请求 return NextResponse.next(); } catch (error) { // 后端请求失败,返回500错误 return new NextResponse("服务器内部错误", { status: 500 }); } } export const config = { matcher: ["/", "/(uk|en)/:path*"], // 保持原国际化匹配规则 };
2. NestJS后端会话验证接口实现
后端需要提供会话验证接口,从Cookie中读取会话ID并返回用户角色信息:
// auth.controller.ts import { Controller, Get, Req, Res, HttpStatus } from '@nestjs/common'; import { AuthService } from './auth.service'; import { Request, Response } from 'express'; @Controller('api/auth') export class AuthController { constructor(private readonly authService: AuthService) {} @Get('validate-session') async validateSession(@Req() req: Request, @Res() res: Response) { // 从Cookie中获取会话ID(需与后端Cookie名称一致) const sessionId = req.cookies['sessionId']; if (!sessionId) { return res.status(HttpStatus.UNAUTHORIZED).send(); } // 调用服务层验证会话有效性 const sessionData = await this.authService.validateSession(sessionId); if (!sessionData) { return res.status(HttpStatus.UNAUTHORIZED).send(); } // 返回包含角色ID的会话数据 return res.status(HttpStatus.OK).json({ adminId: sessionData.adminId, studentId: sessionData.studentId, teacherId: sessionData.teacherId, }); } }
3. 登录页跳转逻辑处理
在登录页面,用户登录成功后可读取redirect参数,跳转到之前访问的受保护路由:
// app/login/page.tsx 'use client'; import { useSearchParams, useRouter } from 'next/navigation'; export default function LoginPage() { const searchParams = useSearchParams(); const router = useRouter(); const redirectUrl = searchParams.get('redirect') || '/'; const handleLogin = async () => { // 执行登录逻辑(调用后端登录接口) // 登录成功后跳转至原请求路径 router.push(redirectUrl); }; return ( <div> <h1>登录</h1> <button onClick={handleLogin}>登录</button> </div> ); }
4. 保留原国际化配置
原i18n.ts配置无需修改,继续用于区域验证与语言包加载。
内容的提问来源于stack exchange,提问作者Nazar Duma
相关产品推荐
相关产品推荐

