如何在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路由间接处理:
- 客户端组件发起请求到你的内部API路由
- API路由在服务端获取令牌,调用第三方服务
- 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
相关产品推荐
相关产品推荐

