如何为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
相关产品推荐
相关产品推荐

