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

NextJS 13.3 App Dir中API路由刷新Token后Cookie生效延迟问题求助

问题排查:NextJS中间件刷新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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 05:07:02