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

基于JWT与刷新令牌的认证及持久化登录方案咨询

问题解答

一、你的认证方案是否可行?

完全可行,这是业界非常成熟的标准认证模式,所谓的矛盾观点大多是对细节防护不到位的担忧,只要做好以下几点就能规避风险:

  • 短生命周期JWT:有效降低JWT泄露后的被滥用窗口,即使被盗用,有效期短也不会造成长期危害
  • 刷新令牌存储:存在HttpOnly、Secure、SameSite=Strict的Cookie中,能避免前端JS读取,从根源防止XSS窃取;同时刷新令牌存在数据库,支持主动吊销(比如用户改密码、登出其他设备),解决了JWT无法主动失效的痛点
  • 额外防护:给刷新令牌设置合理的有效期(比如7天),同时服务端验证刷新令牌时要关联用户ID,防止跨用户复用

二、如何优雅处理JWT自动刷新,避免重复代码?

你提到的两个方案都有明显弊端:方案1每次请求带双令牌冗余且增加服务端校验成本;方案2每个thunk前置检查会导致大量重复逻辑。结合React + tRPC + Redux Toolkit的技术栈,最优解是在tRPC请求链路的全局拦截层统一处理刷新逻辑,具体实现步骤如下:

核心思路

利用tRPC的请求拦截器(interceptor)+ 全局刷新锁,实现一次编码、全局生效的自动刷新,同时避免并发请求重复触发刷新。

具体实现

  1. 客户端JWT过期判断
    用jwt-decode解析Redux store中的JWT,提取exp字段判断是否即将过期(比如提前30秒触发刷新,避免请求过程中过期):
import jwtDecode from 'jwt-decode';

const isTokenExpired = (token) => {
  if (!token) return true;
  const { exp } = jwtDecode(token);
  // 提前30秒判定为过期,预留刷新时间
  return Date.now() >= (exp * 1000) - 30000;
};
  1. tRPC全局请求拦截器
    在创建tRPC客户端时添加请求拦截器,统一处理JWT刷新逻辑,同时用一个全局Promise锁解决并发请求重复刷新的问题:
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router';

const trpc = createTRPCReact<AppRouter>();

// 全局刷新锁:避免多个并发请求同时触发刷新
let refreshPromise: Promise<string | null> | null = null;

const trpcClient = trpc.createClient({
  links: [
    // 自定义请求拦截链路
    (runtime) => ({
      async op(op) {
        const token = store.getState().auth.accessToken;
        let currentToken = token;

        // 如果令牌过期且没有正在进行的刷新请求
        if (isTokenExpired(currentToken) && !refreshPromise) {
          refreshPromise = fetch('/api/auth/refresh', {
            method: 'POST',
            credentials: 'include', // 自动携带HttpOnly Cookie中的刷新令牌
          })
            .then(async (res) => {
              if (!res.ok) {
                // 刷新令牌失效,清除本地状态
                store.dispatch(authActions.logout());
                return null;
              }
              const data = await res.json();
              // 更新Redux store中的新JWT
              store.dispatch(authActions.setAccessToken(data.accessToken));
              return data.accessToken;
            })
            .finally(() => {
              refreshPromise = null;
            });
        }

        // 如果有正在进行的刷新请求,等待其完成
        if (refreshPromise) {
          currentToken = await refreshPromise;
        }

        // 如果最终没有有效令牌,直接返回401错误
        if (!currentToken) {
          return runtime.error({
            error: {
              message: 'Unauthorized',
              code: 'UNAUTHORIZED',
            },
            op,
          });
        }

        // 携带有效令牌发起原请求
        return runtime.next({
          ...op,
          context: {
            ...op.context,
            headers: {
              ...op.context.headers,
              Authorization: `Bearer ${currentToken}`,
            },
          },
        });
      },
    }),
    // 后续的HTTP链路(比如httpLink)
  ],
});
  1. Redux Toolkit状态管理
    在Auth Slice中处理JWT的更新和登出逻辑:
import { createSlice } from '@reduxjs/toolkit';

const authSlice = createSlice({
  name: 'auth',
  initialState: {
    accessToken: localStorage.getItem('accessToken') || null, // 可选:也可以只存在内存,页面刷新时用刷新令牌重新获取
    user: null,
  },
  reducers: {
    setAccessToken: (state, action) => {
      state.accessToken = action.payload;
      // 如果需要持久化到localStorage(注意XSS风险,可选)
      localStorage.setItem('accessToken', action.payload);
    },
    setUser: (state, action) => {
      state.user = action.payload;
    },
    logout: (state) => {
      state.accessToken = null;
      state.user = null;
      localStorage.removeItem('accessToken');
      // 可选:调用服务端接口吊销刷新令牌
      fetch('/api/auth/logout', { credentials: 'include' });
    },
  },
});

export const { setAccessToken, setUser, logout } = authSlice.actions;
export default authSlice.reducer;
  1. 服务端/refresh接口逻辑
    验证Cookie中的刷新令牌,查询数据库确认有效后,生成新的短生命周期JWT返回:
// 示例:Node.js + Express的refresh接口
app.post('/api/auth/refresh', async (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  if (!refreshToken) return res.sendStatus(401);

  try {
    // 从数据库查询刷新令牌是否有效,关联用户ID
    const tokenRecord = await db.refreshTokens.findUnique({
      where: { token: refreshToken },
      include: { user: true },
    });

    if (!tokenRecord || tokenRecord.expiresAt < new Date()) {
      // 刷新令牌无效或过期,清除Cookie
      res.clearCookie('refreshToken', {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'strict',
      });
      return res.sendStatus(401);
    }

    // 生成新的短生命周期JWT
    const newAccessToken = jwt.sign(
      { userId: tokenRecord.user.id },
      process.env.JWT_SECRET,
      { expiresIn: '15m' }
    );

    // 可选:刷新令牌滚动更新(延长有效期)
    await db.refreshTokens.update({
      where: { token: refreshToken },
      data: { expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) },
    });

    res.json({ accessToken: newAccessToken });
  } catch (err) {
    res.sendStatus(500);
  }
});

方案优势

  • 无重复代码:全局拦截器统一处理所有tRPC请求的刷新逻辑,无需在每个thunk或接口中重复编写
  • 并发安全:全局刷新锁避免多个请求同时触发刷新接口,减少服务端压力
  • 侵入性低:业务代码无需感知认证逻辑,专注于业务实现
  • 体验流畅:用户无感知自动刷新令牌,不会因为JWT过期导致请求失败

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 01:55:17