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

NextAuth中RefreshToken无法持久化到ServerSession的问题

NextAuth 仅在服务器会话存储 RefreshToken 的问题

我使用NextAuth时,已成功将accessToken存入服务器会话和客户端会话,但尝试把RefreshToken仅存入服务器会话时,发现只有在session回调中添加它才能在服务器会话里看到,可这样会同时暴露给客户端会话,这不是我想要的结果。

登录时jwt回调中打印的user对象:

user {
  accessToken: 'eyJhbGc...',
  refreshToken: 'eyJhbGc...',
  name: 'Company'
}

服务器会话内容:

server session {
user: { name: 'Company', email: undefined, image: undefined },
accessToken: 'eyJhbGc...',
organisation: { name: 'Company' }
}

我的route.js代码如下:

import NextAuth, { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

import { isJwtExpired } from "../../../../utils/Utils";

const backendBaseUrl = `${process.env.NEXT_PUBLIC_BACKEND_BASE_URL}`;

const handleJWTExpired = async (token) => {
  console.log("Token expired, refreshing");
  const backendBaseUrl = `${process.env.NEXT_PUBLIC_BACKEND_BASE_URL}`;
  const payload = JSON.stringify({
    refresh: token.refreshToken,
  });

  const response = await fetch(`${backendBaseUrl}/api/auth/token/refresh/`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: payload,
  });

  if (response.ok) {
    console.log("Token refreshed");

    // 从DRF后端提取返回的token
    const responseData = await response.json();

    const {
      access: customAccessToken,
      access_expiration: customAccessExpiration,
    } = responseData;

    // 根据当前时间和过期时间计算'iat'和'exp'
    const now = Math.floor(Date.now() / 1000); // 当前Unix时间(秒)
    const expTime = new Date(customAccessExpiration).getTime() / 1000; // 将过期时间转换为Unix时间

    token = {
      ...token,
      accessToken: customAccessToken,
      iat: now,
      exp: expTime,
    };
  } else {
    token = {
      ...token,
      accessToken: "",
    };
    console.log("响应错误", response.status);
  }
  return token;
};

export const authOptions = {
  pages: {
    signIn: "/auth/signin", // 自定义登录页面路径
  },
  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: {
          label: "邮箱",
          type: "text",
          placeholder: "user@username.com",
        },
        password: { label: "密码", type: "password" },
      },
      async authorize(credentials, req) {
        // 携带邮箱和密码请求base_url/api/auth/login/
        const url = `${backendBaseUrl}/api/token/`;
        const response = await fetch(url, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify(credentials),
        });

        if (!response.ok) {
          return null;
        }

        const responseData = await response.json();

        const {
          access: customAccessToken,
          refresh: customRefreshToken,
          organisation: { name },
        } = responseData;

        if (customAccessToken) {
          const responseBody = {
            accessToken: customAccessToken,
            refreshToken: customRefreshToken,
            name: name,
          };

          return responseBody;
        } else {
          return null;
        }
      },
    }),
  ],
  secret: process.env.NEXTAUTH_SECRET,
  callbacks: {
    async jwt({ token, account, trigger, session, profile, user }) {
      // 用新数据更新服务器会话
      if (account && user) {
        console.log('user', user)
        return {
          ...token,
          accessToken: user.accessToken,
          refreshToken: user.refreshToken,
          accessTokenExpires: Date.now() + user.expires_in * 1000, // 假设你有`expires_in`值
        };
      }

      // if (trigger === "update") {
      //   console.log('更新服务器会话...')
      //   return { ...token, ...session.organisation };
      // }

      if (
        typeof token.accessToken === "string" &&
        isJwtExpired(token.accessToken)
      ) {
        token = await handleJWTExpired(token);
      }

      return token;
    },
    async session({ session, token: jwtToken, user, trigger }) {
      const token = jwtToken;
      session.accessToken = token.accessToken;
      // session.refreshToken = token.refreshToken;
      // if (!session.user) {
      //   session.organisation = { name: "" };
      // }

      session.organisation = session.organisation || { name: "" };

      session.organisation.name = token.name || null;
      // session.user.last_name = token.last_name || null;
      // session.user.location = token.location;
      // session.user.username = token.username;

      return session;
    },
  },
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

问题原因

NextAuth的session回调返回的内容会直接发送给客户端,所以在这里添加refreshToken必然会暴露。你看到服务器会话里没有refreshToken,是因为服务器端的JWT已经存储了refreshToken,但通过getServerSession获取的“服务器会话”只是session回调构造的对象,默认不包含未在回调中添加的字段——但这恰恰是安全的,因为refreshToken并没有丢失,只是没被放进返回给客户端的会话对象里。

正确解决方案

  1. 保持现有session回调代码:不要取消注释session.refreshToken = token.refreshToken;,这样客户端永远拿不到refreshToken。
  2. 服务器端使用refreshToken直接从JWT获取:你已经在handleJWTExpired函数中通过token.refreshToken获取了refreshToken,这个逻辑运行在服务器端,完全安全,客户端无法访问到JWT内部的内容。
  3. 验证服务器端是否持有refreshToken:可以在服务器API路由中用getToken函数直接获取JWT内容,确认refreshToken存在:
import { getToken } from "next-auth/jwt";

export async function GET(request) {
  const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET });
  console.log("服务器端JWT内容:", token); // 这里能看到refreshToken
  return new Response(JSON.stringify({ message: "查看控制台输出" }));
}

总结

  • refreshToken已经安全存储在服务器端的JWT中,不需要在session回调中暴露给客户端。
  • 服务器端需要使用refreshToken时,通过getToken或在jwt回调中直接访问token.refreshToken即可,这些操作都在服务器端完成,不会泄露。
  • 你打印的“服务器会话”是session回调构造的对象,它本来就不包含refreshToken,这正是你想要的安全效果。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 23:31:08