You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 00:03:23