Next.js 13新App目录下服务端用户会话值存储方案问询
在Next.js 13 App目录实现服务端会话令牌存储方案
不用回退到Pages目录,App目录完全可以实现你的需求,以下是几种简洁可行的方案:
方案一:自定义Cookie+内存缓存
这是最轻量化的实现方式,无需依赖额外认证库:
- 生成会话标识:当用户从外部系统跳转进来并携带refresh token时,在Route Handler或Server Component中生成GUID,以此作为会话唯一标识。
- 存储令牌映射:在服务端用全局
Map或lru-cache(支持自动过期)存储GUID -> { refreshToken, accessToken }的映射,设置过期时间匹配会话周期。 - 设置客户端Cookie:将GUID设置为带
HttpOnly、Secure(生产环境)、SameSite属性的Cookie,确保客户端每次请求自动携带该标识。 - 令牌检索:在后续的Server Component、Route Handler或Middleware中,读取请求Cookie获取GUID,再从内存缓存中取出对应令牌用于API调用。
示例代码片段:
// 服务端全局缓存(可放在单独的utils文件中) import LRU from 'lru-cache'; const tokenCache = new LRU({ max: 1000, ttl: 24 * 60 * 60 * 1000 // 会话有效期1天 }); // Route Handler处理外部跳转请求 export async function GET(request) { // 从请求头或URL获取refresh token const refreshToken = request.headers.get('X-Refresh-Token') || request.nextUrl.searchParams.get('refresh_token'); // 用refresh token换取access token(替换为你的实际逻辑) const accessToken = await fetchAccessToken(refreshToken); // 生成GUID const sessionId = crypto.randomUUID(); // 存储到缓存 tokenCache.set(sessionId, { refreshToken, accessToken }); // 设置HttpOnly Cookie return new Response('Session initialized', { headers: { 'Set-Cookie': `sessionId=${sessionId}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400` } }); } // 在需要调用API的Server Component中获取令牌 export default async function ProtectedPage() { const sessionId = cookies().get('sessionId')?.value; if (!sessionId) return redirect('/login'); const tokens = tokenCache.get(sessionId); if (!tokens) return redirect('/login'); // 用accessToken调用第三方API const data = await fetch('https://api.example.com/data', { headers: { Authorization: `Bearer ${tokens.accessToken}` } }); return <div>{JSON.stringify(await data.json())}</div>; }
方案二:基于NextAuth.js的内存会话存储
如果想利用NextAuth的会话管理能力,可在App目录中适配:
- 自定义内存适配器,让NextAuth不持久化会话数据,仅存于内存。
- 当用户携带refresh token进来时,在自定义认证回调中处理令牌交换逻辑,将access token和refresh token存入NextAuth会话数据。
- 使用
getServerSession在Server Component或Route Handler中获取会话数据,直接使用其中的令牌。
示例内存适配器简化版:
// app/api/auth/[...nextauth]/route.js import NextAuth from 'next-auth'; import CredentialsProvider from 'next-auth/providers/credentials'; // 内存存储会话 const sessions = new Map(); export const authOptions = { providers: [ CredentialsProvider({ name: 'External Token', credentials: { refreshToken: { label: 'Refresh Token', type: 'text' } }, async authorize(credentials) { if (!credentials?.refreshToken) return null; // 交换access token const accessToken = await fetchAccessToken(credentials.refreshToken); // 返回包含令牌的用户对象 return { id: crypto.randomUUID(), accessToken, refreshToken: credentials.refreshToken }; } }) ], session: { strategy: 'jwt' }, callbacks: { async jwt({ token, user }) { // 首次认证时存入令牌 if (user) { token.accessToken = user.accessToken; token.refreshToken = user.refreshToken; } return token; }, async session({ session, token }) { // 将令牌注入会话 session.user.accessToken = token.accessToken; session.user.refreshToken = token.refreshToken; return session; } }, // 自定义内存适配器(配合database策略使用) adapter: { async createSession(session) { sessions.set(session.sessionToken, session); return session; }, async getSession(sessionToken) { return sessions.get(sessionToken); }, async updateSession(session) { sessions.set(session.sessionToken, session); return session; }, async deleteSession(sessionToken) { sessions.delete(sessionToken); } } }; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };
之后在Server Component中使用:
import { getServerSession } from 'next-auth/next'; import { authOptions } from './api/auth/[...nextauth]/route'; export default async function ProtectedPage() { const session = await getServerSession(authOptions); if (!session) return redirect('/api/auth/signin?refresh_token=...'); const data = await fetch('https://api.example.com/data', { headers: { Authorization: `Bearer ${session.user.accessToken}` } }); return <div>{JSON.stringify(await data.json())}</div>; }
关键注意事项
- 内存缓存仅适用于单实例部署,多实例或Serverless环境需改用分布式缓存(如Redis)共享会话数据。
- 务必给Cookie设置
HttpOnly和Secure属性,避免会话标识被XSS攻击窃取。 - 内存缓存要设置合理的过期时间,防止内存泄漏和无效会话堆积。
内容的提问来源于stack exchange,提问作者David Conlisk
相关产品推荐
相关产品推荐

