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

如何为Next.js服务端请求加自定义头及解决多标签登录Cookie冲突

多标签页用户角色冲突解决方案与实现

核心问题分析

浏览器的Cookie是域名级共享的,多标签页登录不同用户时,旧用户的Cookie未清除、新用户Cookie会覆盖或共存,导致服务端middleware无法区分当前标签页的实际用户,引发角色权限混淆。

可行解决方案:基于JWT oid的Cookie隔离+客户端请求标识

利用JWT中的oid(用户唯一标识)命名用户角色Cookie,结合客户端标签页独立的存储标识,让middleware精准匹配当前用户的权限信息。

一、客户端处理逻辑

1. 登录成功后设置隔离Cookie与标签页标识

登录成功通过MSAL获取令牌后,解析出oid,将角色存储在带oid的Cookie中,同时用sessionStorage存储当前标签页的活跃用户oid(sessionStorage为标签页独立存储,不会跨标签页干扰):

import { msalInstance } from './msalConfig';
import jwtDecode from 'jwt-decode';

// MSAL登录成功回调
export const handleLoginSuccess = async (response) => {
  const decodedToken = jwtDecode(response.accessToken);
  const userOid = decodedToken.oid;
  const userRole = response.account.idTokenClaims.role;

  // 存储角色到带oid的Cookie,配置安全属性
  document.cookie = `currentUser-${userOid}=${JSON.stringify(userRole)}; path=/; domain=your-domain.com; secure; SameSite=Lax`;
  // 存储令牌到HttpOnly Cookie(需后端配合或客户端设置HttpOnly,防止XSS窃取)
  document.cookie = `token-${userOid}=${response.accessToken}; path=/; domain=your-domain.com; secure; HttpOnly; SameSite=Lax`;
  // 标签页独立存储当前活跃用户oid
  sessionStorage.setItem('active-oid', userOid);
  // 设置当前标签页的active-oid Cookie,供middleware识别
  document.cookie = `active-oid=${userOid}; path=/; domain=your-domain.com; SameSite=Lax`;
};

2. 封装API请求自动携带令牌

对所有向服务端发起的API请求,自动添加Authorization头,确保middleware能解析出当前用户的oid:

import { msalInstance } from './msalConfig';
import jwtDecode from 'jwt-decode';

export async function authenticatedFetch(url, options = {}) {
  const activeOid = sessionStorage.getItem('active-oid');
  if (!activeOid) {
    window.location.href = '/login';
    return Promise.reject(new Error('无活跃用户'));
  }

  // 匹配当前oid对应的MSAL账户
  let activeAccount = msalInstance.getActiveAccount();
  if (!activeAccount) {
    const allAccounts = msalInstance.getAllAccounts();
    activeAccount = allAccounts.find(acc => {
      const decoded = jwtDecode(acc.idToken || '');
      return decoded.oid === activeOid;
    });
    if (!activeAccount) {
      window.location.href = '/login';
      return Promise.reject(new Error('未找到匹配账户'));
    }
    msalInstance.setActiveAccount(activeAccount);
  }

  // 静默获取令牌
  const tokenRes = await msalInstance.acquireTokenSilent({
    account: activeAccount,
    scopes: ['api://your-api-scope/.default'] // 替换为实际权限范围
  });

  return fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${tokenRes.accessToken}`,
      'Content-Type': 'application/json'
    }
  });
}

二、Next.js Middleware实现

通过verifyToken函数从请求头或Cookie中获取用户oid,再匹配对应角色Cookie完成权限校验:

import { NextRequest, NextResponse } from 'next/server';
import jwtDecode from 'jwt-decode';
import jwt from 'jsonwebtoken'; // 需安装jsonwebtoken库用于签名验证

// 验证令牌并返回用户oid
function verifyToken(request: NextRequest): { oid: string } | null {
  // 优先从API请求的Authorization头解析令牌
  const authHeader = request.headers.get('Authorization');
  if (authHeader?.startsWith('Bearer ')) {
    const token = authHeader.slice(7);
    try {
      // 验证令牌签名与过期时间(需替换为Azure AD公钥或你的密钥)
      const decoded = jwt.verify(token, 'your-public-key', { algorithms: ['RS256'] });
      if ((decoded as any).exp * 1000 > Date.now()) {
        return { oid: (decoded as any).oid };
      }
    } catch (err) {
      console.error('令牌验证失败:', err);
    }
  }

  // 页面请求从active-oid Cookie获取用户标识
  const activeOid = request.cookies.get('active-oid')?.value;
  if (activeOid) {
    // 验证对应令牌Cookie的有效性
    const tokenCookie = request.cookies.get(`token-${activeOid}`)?.value;
    if (tokenCookie) {
      try {
        const decoded = jwt.verify(tokenCookie, 'your-public-key', { algorithms: ['RS256'] });
        if ((decoded as any).exp * 1000 > Date.now() && (decoded as any).oid === activeOid) {
          return { oid: activeOid };
        }
      } catch (err) {
        console.error('Cookie令牌验证失败:', err);
      }
    }
    // 简化场景下直接返回activeOid(建议保留令牌验证步骤)
    return { oid: activeOid };
  }

  return null;
}

// 权限校验逻辑
function isAuthorized(userRole: string | undefined, request: NextRequest): boolean {
  const path = request.nextUrl.pathname;
  if (path.startsWith('/admin')) {
    return userRole === 'admin';
  }
  if (path.startsWith('/user')) {
    return ['user', 'admin'].includes(userRole);
  }
  return true; // 公共页面直接放行
}

export function middleware(request: NextRequest) {
  const tokenInfo = verifyToken(request);
  if (!tokenInfo) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // 获取当前用户的角色Cookie
  const roleCookie = request.cookies.get(`currentUser-${tokenInfo.oid}`);
  const userRole = roleCookie ? JSON.parse(roleCookie.value) : undefined;

  if (!isAuthorized(userRole, request)) {
    return NextResponse.redirect(new URL('/unauthorized', request.url));
  }

  return NextResponse.next();
}

// 配置需要校验的路由
export const config = {
  matcher: ['/admin/:path*', '/user/:path*', '/dashboard']
};

关键注意事项

  • Cookie安全配置:令牌Cookie必须设置HttpOnly、Secure、SameSite=Lax,防止XSS攻击;角色Cookie若无需客户端读取,也建议设置HttpOnly。
  • 令牌签名验证:middleware中必须验证令牌的签名与过期时间,不能仅依赖oid,避免伪造身份。
  • 多标签页同步登出:可监听storage事件,当某个标签页清除active-oid时,其他标签页触发登出逻辑。

内容的提问来源于stack exchange,提问作者Peter Richards

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 15:48:09