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

Next.js中accessToken、refreshToken、csrfToken的安全存储最佳实践咨询

Next.js 令牌认证存储的最佳实践分析

你的当前方案合理性判断

把refreshToken存在HTTP-only、Secure、SameSite=Strict的Cookie里,这绝对是推荐做法——完全阻断了XSS脚本读取refreshToken的可能,从根源上降低了令牌泄露风险。

但accessToken和csrfToken存在Context Provider(内存)的做法,有可取之处但也存在明显局限,不算最优解。

潜在风险与弊端

  1. 页面刷新/标签页切换导致令牌丢失
    内存中的状态在页面刷新、标签页关闭或新开时会直接清空,用户需要重新触发登录流程(除非你额外做了refreshToken自动刷新逻辑),体验很差。

  2. SSR/SSG场景下的状态不匹配
    Next.js的服务端渲染(SSR)或静态生成(SSG)过程中,无法访问客户端的Context状态,这会导致需要认证的页面在服务端渲染时拿不到用户信息,出现 hydration 不匹配的错误,或者渲染出未登录状态的内容。

  3. csrfToken的暴露风险
    虽然csrfToken本身是用来防CSRF的,但如果被XSS脚本获取到,结合内存中的accessToken,攻击者可以直接发起带合法令牌的API请求,绕开部分防护。

最佳替代实践

1. accessToken:内存存储+自动刷新机制

绝对不要存在localStorage/sessionStorage(XSS易窃取),推荐用内存存储(Context/Zustand/Redux),同时配合refreshToken自动刷新逻辑:

  • 页面加载时,检查内存中是否有accessToken,没有就调用刷新接口,用HTTP-only的refreshToken换取新的accessToken和csrfToken,再存入内存。
  • 给accessToken设置过期时间,提前触发刷新,避免用户操作时令牌失效。

csrfToken需要被客户端JS读取,用来在POST/PUT/DELETE请求中携带(比如放在X-CSRF-Token请求头),所以可以:

  • 把csrfToken存在非HTTP-only、Secure、SameSite=Strict的Cookie里,既可以被JS读取,又能防CSRF。
  • 同时存入内存状态,方便组件快速调用,避免每次都从Cookie读取。

3. 利用Next.js Middleware统一处理认证

用Middleware拦截所有请求,统一处理令牌刷新、权限校验:

  • 检查请求中的refreshToken Cookie,自动刷新accessToken。
  • 把用户信息注入到请求中,让SSR页面也能获取到认证状态,避免客户端状态不一致。

4. 直接用Auth.js(原NextAuth.js)

官方封装的认证库,已经帮你处理了令牌存储、CSRF防护、自动刷新、SSR兼容等所有细节,不用自己造轮子,适合绝大多数场景。

代码示例

用Zustand管理认证状态(含自动刷新)

// store/authStore.js
import { create } from 'zustand';

export const useAuthStore = create((set, get) => ({
  accessToken: null,
  csrfToken: null,
  user: null,
  // 登录成功后设置令牌和用户信息
  setAuthData: ({ accessToken, csrfToken, user }) => set({ accessToken, csrfToken, user }),
  // 刷新令牌逻辑
  refreshToken: async () => {
    try {
      const csrfTokenFromCookie = document.cookie.split('; ').find(row => row.startsWith('csrfToken='))?.split('=')[1];
      const res = await fetch('/api/auth/refresh', {
        method: 'POST',
        headers: {
          'X-CSRF-Token': get().csrfToken || csrfTokenFromCookie,
        },
        credentials: 'include', // 自动携带HTTP-only的refreshToken Cookie
      });

      if (res.ok) {
        const data = await res.json();
        set({ accessToken: data.accessToken, csrfToken: data.csrfToken });
      } else {
        // 刷新失败,清除状态并跳转登录页
        set({ accessToken: null, csrfToken: null, user: null });
        window.location.href = '/login';
      }
    } catch (err) {
      console.error('令牌刷新失败:', err);
      set({ accessToken: null, csrfToken: null, user: null });
      window.location.href = '/login';
    }
  },
}));

页面加载时自动刷新令牌

// app/layout.js (App Router)
'use client';
import { useEffect } from 'react';
import { useAuthStore } from '@/store/authStore';

export default function RootLayout({ children }) {
  const { accessToken, refreshToken } = useAuthStore();

  useEffect(() => {
    // 无accessToken时尝试刷新
    if (!accessToken) {
      refreshToken();
    }
  }, [accessToken, refreshToken]);

  return (
    <html lang="zh-CN">
      <body>{children}</body>
    </html>
  );
}

Middleware拦截认证请求

// middleware.js
import { NextResponse } from 'next/server';

export async function middleware(request) {
  const path = request.nextUrl.pathname;
  const isPublicPath = path === '/login' || path === '/register';
  const refreshToken = request.cookies.get('refreshToken')?.value;

  // 已登录用户访问登录页,跳转首页
  if (isPublicPath && refreshToken) {
    return NextResponse.redirect(new URL('/', request.nextUrl));
  }

  // 未登录用户访问私有页面,跳转登录页
  if (!isPublicPath && !refreshToken) {
    return NextResponse.redirect(new URL('/login', request.nextUrl));
  }

  // 有refreshToken时自动刷新accessToken
  if (refreshToken && !isPublicPath) {
    try {
      const csrfToken = request.cookies.get('csrfToken')?.value;
      const res = await fetch(`${request.nextUrl.origin}/api/auth/refresh`, {
        method: 'POST',
        headers: { 'X-CSRF-Token': csrfToken },
        credentials: 'include',
      });

      if (res.ok) {
        const data = await res.json();
        const response = NextResponse.next();
        // 更新csrfToken Cookie
        response.cookies.set('csrfToken', data.csrfToken, {
          secure: process.env.NODE_ENV === 'production',
          sameSite: 'strict',
          maxAge: 24 * 60 * 60,
        });
        return response;
      }
    } catch (err) {
      return NextResponse.redirect(new URL('/login', request.nextUrl));
    }
  }

  return NextResponse.next();
}

// 匹配所有非API、非静态资源的请求
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 23:35:41