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

React中Refresh Token使用问题:获取新Access Token及页面崩溃修复

React中使用Refresh Token自动刷新Access Token的解决方案

一、处理新Access Token的逻辑

当通过Refresh Token获取到新的Access Token(通常后端会同时返回新的Refresh Token)后,直接调用AuthContext中已有的login方法即可——这个方法已经封装了更新state和localStorage的逻辑,无需重复编写代码。同时要处理刷新失败的情况(比如Refresh Token过期),此时需要执行登出操作。

二、页面刷新时自动修复失效Token

在AuthContext初始化阶段,添加自动刷新Token的逻辑:如果localStorage中存在Refresh Token但Access Token失效,自动调用刷新接口获取新Token。这样页面刷新时就能自动修复无效的Token,避免崩溃。

三、修正组件中的关键问题

1. 修复定时器闭包问题

原setInterval中的reqCounter会陷入闭包陷阱,导致每次递增的值都是初始值,改用函数式更新解决。

2. 修正useEffect依赖

移除不必要的ctx依赖,只保留实际用到的变量,避免不必要的重渲染。

3. 完善错误判断逻辑

原代码仅在catch块设置tokenError,但401/500错误属于HTTP错误,不会触发catch,需要在响应状态判断中主动设置。


修改后的AuthContext代码

import React, { useState, useEffect } from "react";

const AuthContext = React.createContext({
  token: "",
  refreshToken: "",
  isLoggedIn: false,
  login: () => {},
  logout: () => {},
  refreshAuthToken: () => {}, // 新增刷新Token方法
});

export const AuthContextProvider = (props) => {
  let initialToken = localStorage.getItem("token");
  let initialRefreshToken = localStorage.getItem("refresh-token");
  const [token, setToken] = useState(initialToken);
  const [refreshToken, setRefreshToken] = useState(initialRefreshToken);
  const isUserLoggedIn = !!token;

  const logoutHandler = () => {
    setToken(null);
    setRefreshToken(null);
    localStorage.removeItem("token");
    localStorage.removeItem("books");
    localStorage.removeItem("refresh-token");
  };

  const loginHandler = (token, refreshToken) => {
    setToken(token);
    setRefreshToken(refreshToken);
    localStorage.setItem("token", token);
    localStorage.setItem("refresh-token", refreshToken);
  };

  // 新增:刷新Token的方法
  const refreshAuthToken = async () => {
    if (!refreshToken) return;
    try {
      const response = await fetch("你的刷新Token接口", {
        method: "POST",
        body: JSON.stringify({ refresh_token: refreshToken }),
        headers: { "Content-Type": "application/json" },
      });
      if (response.ok) {
        const data = await response.json();
        loginHandler(data.access_token, data.refresh_token); // 假设返回access_token和refresh_token
      } else {
        logoutHandler(); // 刷新失败,直接登出
      }
    } catch (err) {
      logoutHandler();
    }
  };

  // 页面刷新时,自动检查并刷新Token
  useEffect(() => {
    const checkAndRefreshToken = async () => {
      if (!token && refreshToken) {
        await refreshAuthToken();
      }
      // 可根据后端逻辑,添加Token有效性验证(比如调用用户信息接口)
    };
    checkAndRefreshToken();
  }, []);

  const contextValue = {
    token,
    isLoggedIn: isUserLoggedIn,
    refreshToken,
    login: loginHandler,
    logout: logoutHandler,
    refreshAuthToken, // 暴露刷新方法给组件
  };

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

export default AuthContext;

修改后的Books组件代码

import { useContext, useState, useEffect } from "react";
import AuthContext from "./AuthContext";
import BookItem from "./BookItem";
import PulseLoader from "react-spinners/PulseLoader";
import classes from "./Books.module.css";

const Books = () => {
  const ctx = useContext(AuthContext);
  const [books, setBooks] = useState([]);
  const [reqCounter, setReqCounter] = useState(0);
  const [tokenError, setTokenError] = useState(false);
  const [error, setError] = useState(""); // 补全缺失的error状态
  const [isLoading, setIsLoading] = useState(false); // 补全缺失的isLoading状态

  const fetchData = async () => {
    setIsLoading(true);
    setError("");
    try {
      const response = await fetch("你的图书API接口", {
        method: "GET",
        headers: { Authorization: `Bearer ${ctx.token}` },
      });

      if (response.status === 200) {
        const data = await response.json();
        setBooks(data.books);
      } else if (response.status === 404) {
        setError("未找到页面");
      } else if (response.status === 403) {
        setError("你无权访问此页面");
      } else if (response.status === 401 || response.status === 500) {
        // 触发Token刷新逻辑
        setTokenError(true);
      } else {
        setError("请求失败,请稍后重试");
      }
    } catch (err) {
      setError("网络错误,请检查连接");
      setTokenError(true);
    } finally {
      setIsLoading(false);
    }
  };

  // 图书数据请求的useEffect
  useEffect(() => {
    if (ctx.isLoggedIn) {
      fetchData();
    }
  }, [ctx.token, reqCounter, ctx.isLoggedIn]); // 修正依赖,只保留必要变量

  // 定时刷新数据(修复闭包问题)
  useEffect(() => {
    const interval = setInterval(() => {
      setReqCounter((prev) => prev + 1);
    }, 5000);
    return () => clearInterval(interval); // 添加清理函数,避免内存泄漏
  }, []);

  // Token刷新逻辑
  useEffect(() => {
    const refreshTokenAndRetry = async () => {
      if (!tokenError) return;
      await ctx.refreshAuthToken(); // 调用Context中的刷新方法
      if (ctx.isLoggedIn) {
        setReqCounter((prev) => prev + 1); // 刷新成功后,触发图书请求
      }
      setTokenError(false); // 重置错误状态,避免无限循环
    };
    refreshTokenAndRetry();
  }, [tokenError, ctx]);

  const content = books.map((item) => (
    <BookItem
      title={item.name}
      year={item.publish_date}
      pages={item.pages}
      author={item.Author}
      img={item.thumbnail}
      key={item.id || item.name} // 建议用唯一ID,避免名称重复导致的问题
    />
  ));

  return (
    <section className={classes.bookPage}>
      {!error && books.length !== 0 && (
        <ul className={`list ${classes.booksList}`}>{content}</ul>
      )}
      {error && <h2 className={classes.error}>{error}</h2>}
      {isLoading && (
        <PulseLoader color="#f53e3e" className={classes.spinner} />
      )}
    </section>
  );
};

export default Books;

关键说明

  1. 新Token处理:通过调用AuthContext的refreshAuthToken方法,内部会自动调用loginHandler更新state和localStorage,无需手动操作存储。
  2. 页面刷新修复:AuthContext在初始化时会自动检查Refresh Token并尝试刷新,确保页面刷新后Token始终有效。
  3. 避免无限循环:每次Token刷新后重置tokenError,并通过reqCounter触发图书请求,而非直接调用fetchData,保证逻辑清晰。
  4. 内存泄漏防护:给setInterval添加清理函数,组件卸载时清除定时器。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 19:35:18