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

JWT令牌刷新后如何获取认证数据?React项目问题求助

React JWT认证:解决页面加载令牌刷新竞态问题及优化方案

一、解决页面加载时请求提前发起的问题

1. 在AuthContext中维护认证加载状态

在你的认证上下文里加一个isLoading状态,初始设为true。页面加载时先判断本地accessToken是否过期:

  • 若已过期,优先调用updateTokens刷新令牌,完成后把isLoading设为false
  • 若未过期,直接标记加载完成,同时启动定时刷新任务

示例代码:

// AuthContext.js
import { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [accessToken, setAccessToken] = useState(localStorage.getItem('accessToken'));
  const [isLoading, setIsLoading] = useState(true);

  const updateTokens = async () => {
    try {
      const refreshToken = localStorage.getItem('refreshToken');
      const res = await fetch('/api/refresh-token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refreshToken })
      });
      const data = await res.json();
      setAccessToken(data.accessToken);
      localStorage.setItem('accessToken', data.accessToken);
    } catch (err) {
      // 刷新失败则清除令牌,跳转登录页
      localStorage.removeItem('accessToken');
      localStorage.removeItem('refreshToken');
      window.location.href = '/login';
    } finally {
      setIsLoading(false);
    }
  };

  useEffect(() => {
    const checkTokenValidity = () => {
      const token = localStorage.getItem('accessToken');
      if (!token) {
        setIsLoading(false);
        return;
      }
      // 解析token payload判断是否过期
      const payload = JSON.parse(atob(token.split('.')[1]));
      const currentTime = Date.now() / 1000;
      
      if (payload.exp < currentTime) {
        updateTokens();
      } else {
        setIsLoading(false);
        // 提前1分钟刷新,避免刚好在请求时过期
        const refreshTimer = setInterval(updateTokens, (payload.exp - currentTime - 60) * 1000);
        return () => clearInterval(refreshTimer);
      }
    };

    checkTokenValidity();
  }, []);

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

export const useAuth = () => useContext(AuthContext);

2. 页面组件等待加载完成再发起请求

在需要调用getRooms的组件里,先判断isLoading状态,等认证完成后再执行数据请求:

// RoomsPage.js
import { useEffect, useState } from 'react';
import { useAuth } from './AuthContext';

const RoomsPage = () => {
  const { accessToken, isLoading } = useAuth();
  const [rooms, setRooms] = useState([]);

  const fetchRooms = async () => {
    if (!accessToken) return;
    const res = await fetch('/api/rooms', {
      headers: { Authorization: `Bearer ${accessToken}` }
    });
    if (res.ok) {
      const data = await res.json();
      setRooms(data);
    }
  };

  useEffect(() => {
    if (!isLoading) fetchRooms();
  }, [accessToken, isLoading]);

  if (isLoading) {
    return <div>加载中...</div>;
  }

  return (
    <div className="rooms-list">
      {rooms.map(room => <div key={room.id}>{room.name}</div>)}
    </div>
  );
};

export default RoomsPage;

3. 封装请求拦截器统一处理(推荐)

如果用Axios,可以给请求加拦截器,统一处理令牌检查和刷新,避免每个组件重复写逻辑:

// api.js
import axios from 'axios';
import { useAuth } from './AuthContext';

const api = axios.create({ baseURL: '/api' });
// 维护一个刷新Promise,防止并发请求重复刷新
let refreshPromise = null;

api.interceptors.request.use(async (config) => {
  const { accessToken, isLoading, updateTokens } = useAuth();
  
  // 等待认证加载完成
  if (isLoading) {
    await new Promise(resolve => {
      const interval = setInterval(() => {
        if (!isLoading) {
          clearInterval(interval);
          resolve();
        }
      }, 50);
    });
  }

  const token = localStorage.getItem('accessToken');
  if (!token) return config;

  const payload = JSON.parse(atob(token.split('.')[1]));
  const currentTime = Date.now() / 1000;

  // 令牌即将过期且无正在进行的刷新请求,触发刷新
  if (payload.exp < currentTime + 60 && !refreshPromise) {
    refreshPromise = updateTokens().finally(() => {
      refreshPromise = null;
    });
    await refreshPromise;
    // 更新请求头的新令牌
    config.headers.Authorization = `Bearer ${localStorage.getItem('accessToken')}`;
  }

  return config;
});

export default api;

二、更优的认证与令牌刷新方案

1. 替换定时刷新为“请求时检查+自动重试”

不要固定每4分钟刷新,而是在每次请求发起前检查令牌状态:

  • 令牌有效:直接发起请求
  • 令牌过期:先刷新令牌,再用新令牌重试原请求
  • 用拦截器统一处理,减少冗余代码,避免不必要的刷新请求

2. 优化令牌存储与安全策略

  • accessToken:存在内存(Context状态)而非localStorage,降低XSS攻击风险
  • refreshToken:存在HttpOnly Cookie中,由后端通过Cookie接收,避免前端直接操作
  • 后端给refreshToken设置有效期(比如7天),同时维护refreshToken黑名单,支持主动吊销

3. 结合React Suspense处理异步认证(React 18+)

用Suspense让组件等待认证完成后再渲染,替代手动判断isLoading:

// AuthContext.js 改造
export const fetchAuthData = async () => {
  const token = localStorage.getItem('accessToken');
  if (!token) return { accessToken: null };
  
  const payload = JSON.parse(atob(token.split('.')[1]));
  const currentTime = Date.now() / 1000;
  
  if (payload.exp < currentTime) {
    const res = await fetch('/api/refresh-token', {
      method: 'POST',
      credentials: 'include' // 携带HttpOnly Cookie中的refreshToken
    });
    const data = await res.json();
    localStorage.setItem('accessToken', data.accessToken);
    return { accessToken: data.accessToken };
  }
  return { accessToken: token };
};

export const AuthProvider = ({ children }) => {
  const [authData, setAuthData] = useState(null);

  useEffect(() => {
    fetchAuthData().then(setAuthData);
  }, []);

  if (!authData) {
    throw fetchAuthData(); // 抛给Suspense处理加载状态
  }

  return (
    <AuthContext.Provider value={authData}>
      {children}
    </AuthContext.Provider>
  );
};

// 页面使用Suspense
import { Suspense } from 'react';
import RoomsPage from './RoomsPage';

const App = () => {
  return (
    <AuthProvider>
      <Suspense fallback={<div>加载中...</div>}>
        <RoomsPage />
      </Suspense>
    </AuthProvider>
  );
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 14:57:39