NextJS 13.3 App Dir中API路由刷新Token后Cookie生效延迟问题求助
我用NextJS 13.3的App Dir+API Routes结合外部NodeJS后端搭建认证系统,流程如下:
- 前端提交凭证到后端,后端验证后返回accessToken和refreshToken
- NextJS生成包含用户信息及上述Token的新JWT并存在Cookie中
会话验证逻辑:
- 中间件调用内部API路由,解码JWT检查后端accessToken是否有效
- 若accessToken过期,用refreshToken获取新accessToken,API设置新Cookie后返回
success=true,中间件再放行;否则重定向到登录页
当前问题:后端返回新accessToken后,NextJS API设置的Cookie要等中间件响应后才生效,导致用户刷新Token后首次访问页面仍使用旧Cookie,触发禁止错误,需二次刷新才加载新Cookie。
Middleware代码
import { NextResponse } from 'next/server'; import conf from '@/config/conf'; export async function middleware(request) { const accessToken = request.cookies.get('accessToken')?.value; const verifySessionResponse = await fetch(`${conf.BASE_URL}/proxy/auth/verify-session`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ accessToken: accessToken }) }); const json = await verifySessionResponse.json(); if (!json.success) { return NextResponse.rewrite(new URL('/auth/login', request.url)) } return NextResponse.next(verifySessionResponse); } export const config = { matcher: '/dashboard/:path*' }
Route API代码
import conf from '@/config/conf'; import jwt from 'jsonwebtoken'; export async function POST(request) { const { accessToken } = await request.json(); if (!accessToken) { return new Response(JSON.stringify({ success: false, message: 'No access token provided' }), { status: 401 }); } try { const decoded = jwt.verify(accessToken, process.env.JWT_SECRET); const refreshToken = decoded.access.refreshToken.token; const tokenExpires = decoded.access.accessToken.expiresIn; const refreshTokenExpires = decoded.access.refreshToken.expiresIn; if (Date.now() > refreshTokenExpires) { return new Response(JSON.stringify({ success: false, message: 'Refresh token expired' }), { status: 401 }); } if (Date.now() > tokenExpires) { try { const refresh = await fetch(`${conf.API_URL}/auth/refresh-token`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'authorization': `Bearer ${refreshToken}` } }); const refreshJson = await refresh.json(); if (!refreshJson.success) { return new Response(JSON.stringify({ success: false, message: refreshJson.message }), { status: 401 }); } const payout = { id: decoded.id, email: decoded.email, firstName: decoded.firstName, lastName: decoded.lastName, role: decoded.role, access: { accessToken: { token: refreshJson.cookies.accessToken, expiresIn: refreshJson.cookies.accessTokenExpires }, refreshToken: { token: refreshJson.cookies.refreshToken, expiresIn: refreshJson.cookies.refreshTokenExpires, } } }; const newToken = jwt.sign(payout, process.env.JWT_SECRET, { expiresIn: '2d' }); const tokenExpiresDate = new Date(Date.now() + conf.COOKIES_EXPIRES * 1000); // 2 Days console.log('Token refreshed'); const response = new Response(JSON.stringify({ success: true, message: refreshJson.message, token: newToken, expiresIn: tokenExpiresDate }), { status: 200 }); response.headers.set('set-cookie', `accessToken=${newToken}; Path=/; HttpOnly; SameSite=Lax; Expires=${tokenExpiresDate.toUTCString()}`); return response; } catch (err) { console.log(err); return new Response(JSON.stringify({ success: false }), { status: 401 }); } } return new Response(JSON.stringify({ success: true }), { status: 200 }); } catch (err) { return new Response(JSON.stringify({ success: false }), { status: 401 }); } }
问题原因及解决方案
原因
中间件调用内部API后,直接将API响应传给NextResponse.next(),但浏览器不会在中间件阶段处理API返回的Set-Cookie头——中间件的响应是给浏览器的最终页面响应,API的Set-Cookie不会自动合并到最终响应中。同时,当前请求已经带着旧Cookie发起,即使API生成了新Cookie,本次请求的后续流程仍会使用旧Cookie,导致验证失败。
解决步骤
核心思路:让中间件接管Cookie设置逻辑,API只返回新Token及过期信息,由中间件在最终响应中设置Cookie,确保浏览器能即时接收并生效。
修改后的Middleware代码
import { NextResponse } from 'next/server'; import conf from '@/config/conf'; export async function middleware(request) { const accessToken = request.cookies.get('accessToken')?.value; const verifySessionResponse = await fetch(`${conf.BASE_URL}/proxy/auth/verify-session`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ accessToken: accessToken }) }); const json = await verifySessionResponse.json(); if (!json.success) { return NextResponse.rewrite(new URL('/auth/login', request.url)) } // 若API返回新Token,由中间件设置Cookie if (json.token) { const response = NextResponse.next(); response.cookies.set({ name: 'accessToken', value: json.token, expires: json.expiresIn, path: '/', httpOnly: true, sameSite: 'lax' }); return response; } // 无新Token,直接放行 return NextResponse.next(); } export const config = { matcher: '/dashboard/:path*' }
修改后的Route API代码(移除Cookie设置逻辑)
import conf from '@/config/conf'; import jwt from 'jsonwebtoken'; export async function POST(request) { const { accessToken } = await request.json(); if (!accessToken) { return new Response(JSON.stringify({ success: false, message: 'No access token provided' }), { status: 401 }); } try { const decoded = jwt.verify(accessToken, process.env.JWT_SECRET); const refreshToken = decoded.access.refreshToken.token; const tokenExpires = decoded.access.accessToken.expiresIn; const refreshTokenExpires = decoded.access.refreshToken.expiresIn; if (Date.now() > refreshTokenExpires) { return new Response(JSON.stringify({ success: false, message: 'Refresh token expired' }), { status: 401 }); } if (Date.now() > tokenExpires) { try { const refresh = await fetch(`${conf.API_URL}/auth/refresh-token`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'authorization': `Bearer ${refreshToken}` } }); const refreshJson = await refresh.json(); if (!refreshJson.success) { return new Response(JSON.stringify({ success: false, message: refreshJson.message }), { status: 401 }); } const payout = { id: decoded.id, email: decoded.email, firstName: decoded.firstName, lastName: decoded.lastName, role: decoded.role, access: { accessToken: { token: refreshJson.cookies.accessToken, expiresIn: refreshJson.cookies.accessTokenExpires }, refreshToken: { token: refreshJson.cookies.refreshToken, expiresIn: refreshJson.cookies.refreshTokenExpires, } } }; const newToken = jwt.sign(payout, process.env.JWT_SECRET, { expiresIn: '2d' }); const tokenExpiresDate = new Date(Date.now() + conf.COOKIES_EXPIRES * 1000); // 2 Days console.log('Token refreshed'); // 仅返回新Token和过期时间,不设置Cookie return new Response(JSON.stringify({ success: true, message: refreshJson.message, token: newToken, expiresIn: tokenExpiresDate }), { status: 200 }); } catch (err) { console.log(err); return new Response(JSON.stringify({ success: false }), { status: 401 }); } } return new Response(JSON.stringify({ success: true }), { status: 200 }); } catch (err) { return new Response(JSON.stringify({ success: false }), { status: 401 }); } }
额外优化建议
- 减少内部请求:直接在中间件中解码JWT并验证后端accessToken的有效性,避免频繁调用内部API,提升性能。
- 强化Token安全:验证后端返回的refreshToken是否与本地存储的一致,防止Token劫持风险。
内容的提问来源于stack exchange,提问作者Gauts
相关产品推荐
相关产品推荐

