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

React应用前端Refresh Token处理的正确实现与最佳实践指引

全栈应用中Refresh Token的React前端实现问题与最佳实践

背景

我正在开发一款使用Access Token和Refresh Token的全栈应用,已成功实现Access Token的后端逻辑,但在Refresh Token的前端处理上遇到困难。

已完成的后端接口

用户登录返回令牌接口

const accessToken = jwt.sign(
  {
    userId: foundUsername.rows[0].id_persona,
    username: foundUsername.rows[0].username,
    roles: foundRoles.rows,
  },
  process.env.JWT_SECRET_KEY as string,
  { expiresIn: "5m" }
);

const refreshToken = jwt.sign(
  {
    userId: foundUsername.rows[0].id_persona,
    username: foundUsername.rows[0].username,
  },
  process.env.JWT_SECRET_KEY as string,
  {
    expiresIn: "1d",
  }
);

res.cookie("refresh_cookie", refreshToken, {
  httpOnly: true,
  secure: false,
  sameSite: "none",
  maxAge: 24 * 60 * 60 * 1000,
}); 
return res.json({ accessToken });

刷新Access Token接口

export const refreshAccess = async (req: Request, res: Response) => {
  // Check if the token exists
  const token = req.cookies["refresh_token"];
  if (!token) {
    return res.status(401).json({ message: "Unauthorized" });
  }

  try {
    const decoded = jwt.verify(
      token,
      process.env.JWT_SECRET_KEY as string
    ) as JwtPayload;
    const result = await pool.query(
      "SELECT id_roles FROM assignated_roles WHERE id_persona = $1",
      [decoded.userId]
    );

    const foundRoles = result.rows.map((row) => row.id_roles);
    const accessToken = jwt.sign(
      {
        userId: decoded.userId,
        username: decoded.username,
        roles: foundRoles,
      },
      process.env.JWT_SECRET_KEY as string,
      { expiresIn: "5m" }
    );

    return res.json({ accessToken });
  } catch (err) {
    return res.status(403).json({ message: "Invalid token" });
  }
};

注:后端刷新接口读取的cookie名称为refresh_token,但登录接口设置的cookie名称是refresh_cookie,需保持一致,否则刷新接口无法正确获取Refresh Token。

前端困惑点

  • 是否需要用定时器自动刷新Access Token?
  • 如何用Context管理令牌,实现受保护路由?
  • 担心刷新接口会频繁请求数据库,有没有优化方案?

React前端实现最佳实践与具体指引

1. 令牌管理:Context + LocalStorage 统一维护

创建AuthContext封装登录状态、Access Token的读写逻辑,实现全局状态共享:

import { createContext, useContext, useState, useEffect, ReactNode } from 'react';

interface AuthContextType {
  accessToken: string | null;
  login: (token: string) => void;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export const AuthProvider = ({ children }: { children: ReactNode }) => {
  const [accessToken, setAccessToken] = useState<string | null>(
    localStorage.getItem('accessToken')
  );

  const login = (token: string) => {
    setAccessToken(token);
    localStorage.setItem('accessToken', token);
  };

  const logout = () => {
    setAccessToken(null);
    localStorage.removeItem('accessToken');
    // 可选:调用后端接口清除refresh cookie
    fetch('/api/logout', { method: 'POST' });
  };

  return (
    <AuthContext.Provider value={{ accessToken, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth必须在AuthProvider内部使用');
  }
  return context;
};

2. 刷新Access Token的两种方案

方案一:请求拦截+过期自动刷新(推荐)

无需定时器,在请求拦截器中检查Access Token有效期,即将过期时自动刷新,同时处理并发请求避免重复刷新:

import axios from 'axios';

const api = axios.create({ baseURL: '/api' });

// 防止并发请求重复刷新token
let isRefreshing = false;
let failedQueue: { resolve: (token: string) => void; reject: (error: any) => void }[] = [];

const processQueue = (error: any, token: string | null = null) => {
  failedQueue.forEach(prom => {
    error ? prom.reject(error) : prom.resolve(token!);
  });
  failedQueue = [];
};

// 请求拦截器
api.interceptors.request.use(async (config) => {
  const token = localStorage.getItem('accessToken');
  if (!token) return config;

  // 解析token获取过期时间
  const decoded = JSON.parse(atob(token.split('.')[1]));
  const expiresAt = decoded.exp * 1000;

  // 剩余时间小于1分钟则刷新token
  if (expiresAt - Date.now() < 60 * 1000) {
    if (!isRefreshing) {
      isRefreshing = true;
      try {
        const res = await axios.post('/api/refresh');
        const newToken = res.data.accessToken;
        localStorage.setItem('accessToken', newToken);
        config.headers['Authorization'] = `Bearer ${newToken}`;
        processQueue(null, newToken);
      } catch (err) {
        processQueue(err);
        localStorage.removeItem('accessToken');
        window.location.href = '/login';
      } finally {
        isRefreshing = false;
      }
    } else {
      // 等待刷新完成后再继续请求
      return new Promise((resolve, reject) => {
        failedQueue.push({ resolve, reject });
      }).then(newToken => {
        config.headers['Authorization'] = `Bearer ${newToken}`;
        return config;
      });
    }
  } else {
    config.headers['Authorization'] = `Bearer ${token}`;
  }
  return config;
});

// 响应拦截器处理401错误
api.interceptors.response.use(res => res, async (error) => {
  const originalRequest = error.config;
  if (error.response.status === 401 && !originalRequest._retry) {
    originalRequest._retry = true;
    try {
      const res = await axios.post('/api/refresh');
      const newToken = res.data.accessToken;
      localStorage.setItem('accessToken', newToken);
      originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
      return api(originalRequest);
    } catch (err) {
      localStorage.removeItem('accessToken');
      window.location.href = '/login';
      return Promise.reject(err);
    }
  }
  return Promise.reject(error);
});

export default api;

方案二:定时器定时刷新

适合对实时性要求不高的场景,需注意组件卸载时清理定时器:

import { useEffect } from 'react';
import { useAuth } from './AuthContext';

const useTokenRefresh = () => {
  const { accessToken, login } = useAuth();

  useEffect(() => {
    if (!accessToken) return;

    const decoded = JSON.parse(atob(accessToken.split('.')[1]));
    // 提前30秒刷新token
    const refreshTime = decoded.exp * 1000 - Date.now() - 30 * 1000;

    const timer = setTimeout(async () => {
      try {
        const res = await fetch('/api/refresh', { method: 'POST' });
        if (res.ok) {
          const data = await res.json();
          login(data.accessToken);
        } else {
          throw new Error('刷新失败');
        }
      } catch (err) {
        useAuth().logout();
      }
    }, refreshTime);

    return () => clearTimeout(timer);
  }, [accessToken, login]);
};

export default useTokenRefresh;

3. 受保护路由实现

结合React Router和AuthContext,实现未登录时重定向到登录页:

import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from './AuthContext';

const ProtectedRoute = () => {
  const { accessToken } = useAuth();

  if (!accessToken) {
    return <Navigate to="/login" replace />;
  }

  // 前端校验token是否过期
  const decoded = JSON.parse(atob(accessToken.split('.')[1]));
  if (decoded.exp * 1000 < Date.now()) {
    useAuth().logout();
    return <Navigate to="/login" replace />;
  }

  return <Outlet />;
};

export default ProtectedRoute;

4. 数据库请求优化建议

  • 缓存角色信息:后端用Redis缓存用户ID对应的角色列表,设置1小时左右的有效期,避免每次刷新都查询数据库。
  • 角色信息存入Refresh Token:若用户角色不频繁变更,可将角色列表直接写入Refresh Token的payload中,刷新时无需查询数据库,直接解析Refresh Token即可获取角色。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 18:15:57