Next.js 13.4中SSR组件无法更新HTTP-only Cookie的问题
问题根源
SSR请求在Next.js服务器端发起,Axios拦截器运行在服务器端时,刷新令牌后服务器返回的Set-Cookie头只会作用于服务器端的请求上下文,无法直接同步到客户端浏览器的Cookie中。而客户端组件的请求在浏览器中发送,服务器返回的Set-Cookie会被浏览器自动处理,更新HTTP-only Cookie。
解决方案
方案1:将授权请求迁移到客户端(推荐)
如果业务允许,把需要授权的数据获取逻辑放到客户端组件中,使用SWR、React Query等数据获取库,让请求在浏览器环境发起。这样刷新令牌时,服务器返回的Set-Cookie头会被浏览器自动接收并更新HTTP-only Cookie。
示例代码(用SWR实现):
"use client"; import useSWR from 'swr'; import axios from 'axios'; // 配置Axios拦截器处理令牌刷新 axios.interceptors.response.use( (res) => res, async (err) => { const originalReq = err.config; if (err.response.status === 401 && !originalReq._retry) { originalReq._retry = true; // 调用刷新令牌接口 await axios.post('/api/auth/refresh', {}, { withCredentials: true }); // 重新发起原请求 return axios(originalReq); } return Promise.reject(err); } ); const fetcher = (url) => axios.get(url, { withCredentials: true }).then(res => res.data); export default function UserProfile() { const { data: user, error } = useSWR('/api/user', fetcher); if (error) return <div>加载用户信息失败</div>; if (!user) return <div>加载中...</div>; return <div>欢迎回来,{user.username}</div>; }
方案2:用Next.js Middleware统一处理令牌刷新
Middleware会在所有请求(包括SSR、客户端请求)到达页面/API路由前运行,可以在这里统一检查access token是否过期,调用刷新接口后将新的Set-Cookie头传递给客户端,确保Cookie在请求进入SSR逻辑前就已更新。
示例代码(middleware.ts):
import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import jwtDecode from 'jwt-decode'; export async function middleware(request: NextRequest) { const accessToken = request.cookies.get('access_token'); const refreshToken = request.cookies.get('refresh_token'); let isTokenExpired = false; // 解码access token判断是否过期 if (accessToken) { try { const decoded = jwtDecode<{ exp: number }>(accessToken.value); isTokenExpired = Date.now() >= decoded.exp * 1000; } catch (err) { isTokenExpired = true; } } if (isTokenExpired && refreshToken) { // 调用NestJS刷新令牌接口 const refreshRes = await fetch(`${process.env.NEST_API_BASE_URL}/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: request.headers.get('Cookie') || '', }, credentials: 'include', body: JSON.stringify({ refreshToken: refreshToken.value }), }); if (refreshRes.ok) { // 获取新的Cookie头并设置到响应中 const newCookies = refreshRes.headers.getSetCookie(); const response = NextResponse.next(); newCookies.forEach(cookie => response.headers.append('Set-Cookie', cookie)); return response; } else { // 刷新失败,跳转到登录页 return NextResponse.redirect(new URL('/login', request.url)); } } return NextResponse.next(); } // 指定需要拦截的授权路由 export const config = { matcher: ['/profile', '/dashboard', '/api/user/:path*'], };
方案3:在SSR数据获取逻辑中手动同步Cookie
如果必须在SSR组件中发起请求,可在getServerSideProps中处理令牌刷新,并将新的Set-Cookie头通过响应传递给客户端。
示例代码(getServerSideProps):
export async function getServerSideProps(context) { const apiBaseUrl = process.env.NEST_API_BASE_URL; const cookieHeader = context.req.headers.cookie || ''; // 尝试获取用户信息 let userRes = await fetch(`${apiBaseUrl}/api/user`, { headers: { Cookie: cookieHeader }, credentials: 'include', }); // 401表示access token过期,尝试刷新 if (userRes.status === 401) { const refreshRes = await fetch(`${apiBaseUrl}/auth/refresh`, { method: 'POST', headers: { Cookie: cookieHeader, 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ refreshToken: context.req.cookies.refresh_token }), }); if (refreshRes.ok) { // 获取新的Cookie并设置到响应头,同步给客户端 const newCookies = refreshRes.headers.getSetCookie(); newCookies.forEach(cookie => context.res.setHeader('Set-Cookie', cookie)); // 用新的Cookie重新请求用户信息 userRes = await fetch(`${apiBaseUrl}/api/user`, { headers: { Cookie: newCookies.map(c => c.split(';')[0]).join('; ') }, credentials: 'include', }); } else { // 刷新失败,跳转到登录页 return { redirect: { destination: '/login', permanent: false }, }; } } const user = await userRes.json(); return { props: { user } }; }
总结
- 优先选择方案1,客户端数据获取更简单,且天然适配HTTP-only Cookie的更新逻辑。
- 如果需要统一处理所有授权请求,方案2的Middleware是最优解,无需在每个页面重复编写刷新逻辑。
- 方案3仅适用于必须在SSR中获取授权数据的场景,代码繁琐且维护成本高。
内容的提问来源于stack exchange,提问作者Laptev Ilia
相关产品推荐
相关产品推荐

