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

如何在NextAuth中安全存储accessToken与refreshToken?

NextAuth中安全存储accessToken的最佳实践与全局访问方案

问题根源

你当前在session回调中返回accessToken和refreshToken,会导致这些敏感令牌被包含在返回给前端的会话数据里,浏览器可直接读取,存在泄露风险。

安全存储的核心原则

敏感令牌绝对不能返回给前端,必须仅在服务端存储和处理。NextAuth提供两种核心方案实现这一点:

1. 基于JWT的服务端存储

使用JWT策略时,令牌会被加密存储在HttpOnly Cookie中(前端无法读取),通过jwt回调处理令牌的获取、刷新逻辑,session回调仅返回非敏感用户信息。

示例配置代码:

export const authOptions = {
  providers: [/* 你的登录提供商配置,比如Google、GitHub等 */],
  session: {
    strategy: "jwt", // 启用JWT会话策略
  },
  callbacks: {
    async jwt({ token, user, account }) {
      // 首次登录时,从account中提取令牌存入JWT
      if (user && account) {
        token.accessToken = account.access_token;
        token.refreshToken = account.refresh_token;
        // 计算令牌过期时间(单位:毫秒)
        token.accessTokenExpires = Date.now() + account.expires_at * 1000;
      }

      // 令牌未过期,直接返回
      if (Date.now() < token.accessTokenExpires) {
        return token;
      }

      // 令牌即将过期,执行刷新逻辑
      return refreshAccessToken(token);
    },
    async session({ session, token }) {
      // 仅向前端返回非敏感用户数据,不包含令牌
      session.user.id = token.id;
      session.user.name = token.name;
      session.user.email = token.email;
      return session;
    },
  },
};

// 自定义令牌刷新函数
async function refreshAccessToken(token) {
  try {
    const response = await fetch("你的身份提供商刷新令牌接口", {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        client_id: process.env.AUTH_CLIENT_ID,
        client_secret: process.env.AUTH_CLIENT_SECRET,
        grant_type: "refresh_token",
        refresh_token: token.refreshToken,
      }),
    });

    const refreshedTokens = await response.json();
    if (!response.ok) throw refreshedTokens;

    return {
      ...token,
      accessToken: refreshedTokens.access_token,
      refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
      accessTokenExpires: Date.now() + refreshedTokens.expires_at * 1000,
    };
  } catch (error) {
    // 刷新失败时标记错误,后续可在业务逻辑中处理
    return { ...token, error: "RefreshAccessTokenError" };
  }
}

2. 基于数据库适配器的持久化存储

如果需要持久化会话记录,可以使用NextAuth的数据库适配器(如Prisma、TypeORM),令牌会被存储在服务端数据库中,前端无法直接访问。

配置要点:

  • 安装对应数据库适配器并配置adapter选项
  • 同样不在session回调中返回令牌,仅返回用户基本信息
  • 令牌的刷新逻辑仍在jwt回调中处理,或直接从数据库会话记录中读取

全局访问令牌的正确方式

服务端场景(API路由、服务端组件)

直接在服务端通过getServerSession或getToken获取令牌,用于调用第三方API:

import { getServerSession } from "next-auth/next";
import { getToken } from "next-auth/jwt";
import { authOptions } from "../auth/[...nextauth]";

export async function GET() {
  // 获取会话(可选,用于验证用户是否登录)
  const session = await getServerSession(authOptions);
  if (!session) return new Response("未授权", { status: 401 });

  // 从JWT中获取accessToken
  const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
  const accessToken = token.accessToken;

  // 使用令牌调用第三方服务
  const apiResponse = await fetch("第三方API地址", {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  const data = await apiResponse.json();

  return Response.json(data);
}

客户端场景(客户端组件)

禁止直接在客户端获取令牌,应通过调用自己的API路由间接处理:

  1. 客户端组件发起请求到你的内部API路由
  2. API路由在服务端获取令牌,调用第三方服务
  3. API路由将结果返回给客户端组件

示例客户端组件代码:

"use client";

import { useEffect, useState } from "react";

export default function ClientComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      // 调用内部API路由,由服务端处理令牌
      const response = await fetch("/api/third-party-data");
      const result = await response.json();
      setData(result);
    }
    fetchData();
  }, []);

  return <div>{data ? JSON.stringify(data) : "加载中..."}</div>;
}

关键注意事项

  • 所有敏感配置(如client_id、client_secret、NEXTAUTH_SECRET)必须存在服务端环境变量中,不要暴露给前端
  • 确保session回调中绝不返回accessToken或refreshToken
  • 令牌刷新逻辑要容错,避免因刷新失败导致用户会话中断

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 04:00:14