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

NextJS(App Router)无法修改Access/Refresh Token Cookie问题求助

NextJS App Router + Next-Auth Token自动刷新问题排查与解决

核心问题梳理

在NextJS App Router项目中,使用Next-Auth对接独立后端获取Access/Refresh Token,尝试将Token存入Cookie并通过Axios拦截器实现过期自动刷新,但Token更新操作始终失败。已尝试两种方案:Server Action调用cookies().set报错,API端点返回成功但Cookie无实际更新。

已尝试方案的问题分析

1. Server Action修改Cookie报错

报错本质是Cookie属性配置冲突或操作上下文不符合要求:

  • 未保证更新Cookie时的属性(httpOnly/secure/sameSite/path)与登录时完全一致,浏览器会拒绝覆盖不匹配的Cookie;
  • Server Action虽标注'use server',但如果是在非POST触发的场景下调用(比如直接在组件渲染时执行),也会触发Cookie修改权限错误。

2. API端点更新Token无效果

常见原因:

  • 使用GET请求修改Cookie,违反HTTP状态修改规范,浏览器会忽略此类操作;
  • Cookie属性与旧值不一致,导致浏览器创建新Cookie而非覆盖原有值;
  • Axios调用API时未开启withCredentials: true,服务器返回的Set-Cookie头无法被浏览器保存。

正确解决方案

1. 利用Next-Auth内置机制管理Token(推荐)

放弃手动操作Cookie,通过Next-Auth的jwt和session回调自动处理Token刷新,避免自定义Cookie与Next-Auth Session冲突:

// options.ts
import type { NextAuthOptions } from 'next-auth';

export const authOptions: NextAuthOptions = {
  providers: [/* 你的登录配置 */],
  session: { strategy: 'jwt' },
  callbacks: {
    async jwt({ token, user, account }) {
      // 首次登录时保存后端返回的Token
      if (user && account) {
        token.accessToken = account.access_token;
        token.refreshToken = account.refresh_token;
        token.expiresAt = Date.now() + (account.expires_in as number) * 1000;
      }

      // 检查Access Token是否即将过期
      if (Date.now() < (token.expiresAt as number)) {
        return token;
      }

      // 调用后端刷新Token
      try {
        const response = await fetch(`${process.env.API_URL}/refresh-token`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ refreshToken: token.refreshToken }),
        });

        if (!response.ok) throw new Error('Token刷新失败');
        const { access_token, refresh_token, expires_in } = await response.json();

        return {
          ...token,
          accessToken: access_token,
          refreshToken: refresh_token,
          expiresAt: Date.now() + expires_in * 1000,
        };
      } catch (err) {
        // 刷新失败,清空Token触发登出
        return { ...token, accessToken: null, refreshToken: null };
      }
    },
    async session({ session, token }) {
      // 将Token注入Session,供客户端使用
      session.user.accessToken = token.accessToken;
      return session;
    },
  },
};

2. Axios拦截器正确实现

客户端拦截器需通过Next-Auth的useSession获取Token,避免直接读取httpOnly Cookie:

// utils/api.ts
import axios from 'axios';
import { useSession, signOut } from 'next-auth/react';
import { useEffect } from 'react';

const api = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL });

export function useApi() {
  const { data: session, update } = useSession();

  useEffect(() => {
    // 请求拦截器:添加Token到请求头
    const requestInterceptor = api.interceptors.request.use(
      (config) => {
        if (session?.user?.accessToken) {
          config.headers.Authorization = `Bearer ${session.user.accessToken}`;
        }
        return config;
      },
      (err) => Promise.reject(err)
    );

    // 响应拦截器:处理Token过期
    const responseInterceptor = api.interceptors.response.use(
      (res) => res,
      async (err) => {
        const originalRequest = err.config;
        // 避免重复重试
        if (err.response?.status === 401 && !originalRequest._retry) {
          originalRequest._retry = true;
          try {
            // 调用Next-Auth的Session更新,触发jwt回调刷新Token
            const newSession = await update();
            if (newSession?.user?.accessToken) {
              originalRequest.headers.Authorization = `Bearer ${newSession.user.accessToken}`;
              return api(originalRequest);
            } else {
              await signOut();
              return Promise.reject(err);
            }
          } catch (refreshErr) {
            await signOut();
            return Promise.reject(refreshErr);
          }
        }
        return Promise.reject(err);
      }
    );

    return () => {
      api.interceptors.request.eject(requestInterceptor);
      api.interceptors.response.eject(responseInterceptor);
    };
  }, [session, update]);

  return api;
}

3. 手动操作Cookie的正确姿势(若必须自定义)

如果坚持手动管理Cookie,需保证所有操作的属性完全一致:

Server Action示例

// actions/refreshToken.ts
'use server';

import { cookies } from 'next/headers';

export async function refreshToken() {
  const refreshToken = cookies().get('refresh_token')?.value;
  if (!refreshToken) throw new Error('无刷新Token');

  const response = await fetch(`${process.env.API_URL}/refresh-token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ refreshToken }),
  });

  if (!response.ok) throw new Error('刷新失败');
  const { access_token, refresh_token } = await response.json();

  // 严格匹配登录时的Cookie属性
  cookies().set('access_token', access_token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: 3600, // 与Access Token有效期一致
  });
  cookies().set('refresh_token', refresh_token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: 60 * 60 * 24 * 7, // 7天有效期
  });

  return { success: true };
}

API Route示例(必须用POST)

// app/api/refresh-token/route.ts
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';

export async function POST(request: Request) {
  const { refreshToken } = await request.json();
  const response = await fetch(`${process.env.API_URL}/refresh-token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ refreshToken }),
  });

  if (!response.ok) {
    return NextResponse.json({ error: '刷新失败' }, { status: 401 });
  }

  const { access_token, refresh_token } = await response.json();
  const nextRes = NextResponse.json({ success: true });

  // 同步Cookie属性
  nextRes.cookies.set('access_token', access_token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: 3600,
  });
  nextRes.cookies.set('refresh_token', refresh_token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: 60 * 60 * 24 * 7,
  });

  return nextRes;
}

关键遗漏点总结

  1. Cookie属性不匹配:更新Cookie时未与登录时的httpOnly/secure/sameSite/path完全一致,导致浏览器拒绝覆盖;
  2. 未利用Next-Auth内置机制:手动操作Cookie与Next-Auth的Session管理冲突,增加复杂度;
  3. Axios拦截器逻辑错误:客户端无法读取httpOnly Cookie,需通过Next-Auth Session获取Token,且未设置重试标记导致无限循环;
  4. 请求方法违规:用GET请求修改Cookie,违反HTTP规范,浏览器不处理此类操作。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 04:56:03