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

Next.js App Router中JWT安全管理:Middleware等方案该如何选择?

Next.js App Router 中JWT会话令牌存储的最佳实践

针对你提到的三个核心场景(Middleware路由保护、Server Components数据获取、Client Components交互UI),结合安全性与性能要求,以下是明确的实践方案和对各选项的分析:

核心适配原则

Next.js的服务端-客户端混合架构下,优先选择服务端可控的存储方式,避免敏感令牌暴露在客户端JS可访问的环境中,同时确保各场景能高效获取令牌完成授权。


各选项分析与落地方案

1. HttpOnly Cookies(首选方案)

优势

  • 抗XSS攻击能力最强:令牌存储在HttpOnly、SameSite=Strict的Cookie中,客户端JS无法读取,从根源避免XSS窃取风险。
  • 全场景适配:
    • Middleware:直接通过request.cookies获取令牌,路由保护逻辑可以同步执行,无额外依赖。
    • Server Components:用Next.js内置的cookies()函数读取令牌,数据请求时直接携带到后端。
    • Client Components:无需手动传递令牌,发起请求时浏览器会自动携带Cookie;如果用Server Actions,框架会自动在请求中包含Cookie,无需额外处理。

令牌刷新的延迟解决

你担心的刷新周期延迟问题,可以通过以下方式优化:

  • 短访问令牌+独立刷新令牌:将访问令牌有效期设为15-30分钟,刷新令牌设为7-30天(同样存在HttpOnly Cookie中)。Middleware仅校验访问令牌的有效性,刷新逻辑放在API Route或Server Action中。
  • 异步提前刷新:在Middleware中检查访问令牌的过期时间,当令牌快过期(比如剩余1分钟)时,异步触发刷新请求(不阻塞当前用户请求),刷新成功后更新Cookie中的访问令牌。用户当前请求仍使用旧令牌,下一次请求自动使用新令牌,完全无感知。

示例Middleware逻辑:

export async function middleware(request) {
  const accessToken = request.cookies.get('access_token')?.value;
  const refreshToken = request.cookies.get('refresh_token')?.value;
  
  // 自定义令牌过期校验逻辑(解析JWT的exp字段)
  const isAccessTokenExpired = accessToken ? Date.now() > JSON.parse(atob(accessToken.split('.')[1])).exp * 1000 : true;

  if (!accessToken) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // 异步刷新令牌,不阻塞当前请求
  if (isAccessTokenExpired && refreshToken) {
    try {
      const res = await fetch(`${process.env.BACKEND_URL}/auth/refresh`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refreshToken }),
        credentials: 'include'
      });

      if (res.ok) {
        const { newAccessToken, newRefreshToken } = await res.json();
        // 更新Cookie,注意设置HttpOnly、SameSite、Secure等属性
        const response = NextResponse.next();
        response.cookies.set('access_token', newAccessToken, {
          httpOnly: true,
          secure: process.env.NODE_ENV === 'production',
          sameSite: 'strict',
          maxAge: 15 * 60 // 15分钟
        });
        response.cookies.set('refresh_token', newRefreshToken, {
          httpOnly: true,
          secure: process.env.NODE_ENV === 'production',
          sameSite: 'strict',
          maxAge: 7 * 24 * 60 * 60 // 7天
        });
        return response;
      }
    } catch (err) {
      // 刷新失败,跳转到登录页
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }

  return NextResponse.next();
}

2. LocalStorage(不推荐)

  • 安全风险极高:XSS攻击可以直接通过localStorage.getItem()窃取令牌,尤其是第三方CMS、分析脚本等供应链攻击日益普遍,一旦脚本被注入恶意代码,令牌会被直接泄露。
  • 场景适配困难:Server Components和Middleware无法直接读取LocalStorage中的令牌,必须由Client Components先将令牌传递到服务端,这会导致服务端数据请求的延迟,还容易出现未授权的中间状态。
  • 结论:除非你的应用完全没有服务端场景,否则绝对不要使用LocalStorage存储JWT。

3. NextAuth.js/Auth.js(适配自定义后端的高效方案)

你不需要因为使用自定义后端就放弃Auth.js,它完全支持对接自定义登录接口并管理JWT:

  • 核心优势:自动处理令牌存储(默认用HttpOnly Cookies)、刷新逻辑、路由保护,完美适配Next.js所有场景,减少重复造轮子的工作量。
  • 落地方式:配置CredentialsProvider,在authorize函数中调用你的后端登录接口获取JWT,然后通过Auth.js的回调函数管理令牌的刷新和会话同步。

示例配置:

// app/api/auth/[...nextauth]/route.js
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';

export const authOptions = {
  providers: [
    CredentialsProvider({
      name: 'Custom Auth',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' }
      },
      async authorize(credentials) {
        // 调用自定义后端登录接口
        const res = await fetch(`${process.env.BACKEND_URL}/auth/login`, {
          method: 'POST',
          body: JSON.stringify(credentials),
          headers: { 'Content-Type': 'application/json' }
        });
        const userData = await res.json();

        if (res.ok && userData.accessToken) {
          // 返回包含JWT的用户信息
          return {
            id: userData.userId,
            email: userData.email,
            accessToken: userData.accessToken,
            refreshToken: userData.refreshToken,
            expiresAt: Date.now() + userData.expiresIn * 1000
          };
        }
        return null;
      }
    })
  ],
  session: { strategy: 'jwt' },
  callbacks: {
    async jwt({ token, user }) {
      // 登录时初始化令牌
      if (user) {
        token.accessToken = user.accessToken;
        token.refreshToken = user.refreshToken;
        token.expiresAt = user.expiresAt;
      }

      // 提前1分钟刷新令牌
      if (Date.now() > token.expiresAt - 60 * 1000) {
        try {
          const res = await fetch(`${process.env.BACKEND_URL}/auth/refresh`, {
            method: 'POST',
            body: JSON.stringify({ refreshToken: token.refreshToken }),
            headers: { 'Content-Type': 'application/json' }
          });
          const newTokens = await res.json();
          if (res.ok) {
            token.accessToken = newTokens.accessToken;
            token.expiresAt = Date.now() + newTokens.expiresIn * 1000;
          } else {
            // 刷新失败,清除令牌
            return {};
          }
        } catch (err) {
          return {};
        }
      }
      return token;
    },
    async session({ session, token }) {
      // 将令牌同步到客户端会话
      session.user.accessToken = token.accessToken;
      return session;
    }
  }
};

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

配置完成后:

  • Middleware:用getServerSession(authOptions)获取会话,实现路由保护。
  • Server Components:调用getServerSession(authOptions)获取令牌,用于数据请求。
  • Client Components:用useSession()钩子获取会话,交互请求时携带令牌(或直接用Server Actions自动传递Cookie)。

总结

  1. 优先选择HttpOnly Cookies,无论是自己实现还是通过Auth.js托管,都是兼顾安全和性能的最优解。
  2. 绝对避免使用LocalStorage存储JWT,安全风险不可接受。
  3. 如果不想自己实现令牌刷新和会话管理逻辑,Auth.js是适配自定义后端的高效工具,无需重构现有后端接口。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.01 12:29:54