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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 10:02:39