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

使用Redux-Toolkit AsyncThunk刷新Access Token遇重复执行问题求助

问题描述

我是Redux-Toolkit新手,当前在logoutUser asyncThunk中,当检测到返回错误码为401(Access Token过期)时,会调用refreshUserToken thunk获取新的刷新令牌,之后重试logoutUser,但这导致logoutThunk执行了两次。请问该如何修正这个问题?或者有没有更优的使用asyncThunks处理令牌刷新的方式?

注:我使用Cookie与服务器交互发送和接收Access Token及Refresh Token。

相关代码

User Slice代码

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axiosInstance from "../utils/axiosInstance";

export const logoutUser = createAsyncThunk(
  "users/logoutUser",
  async (_, { rejectWithValue, dispatch }) => {
    try {
      const response = await axiosInstance.post("/users/logout");
      return response.data;
    } catch (error) {
      console.log(error.response.status);
      if (error.response.status === 401) {
        try {
          await dispatch(refreshUserToken());

          const retryResponse = await dispatch(logoutUser());
          return retryResponse;
        } catch (refreshError) {
          return rejectWithValue(refreshError.message);
        }
      } else {
        return rejectWithValue(error.message);
      }
    }
  }
);

export const refreshUserToken = createAsyncThunk(
  "users/refreshUserToken",
  async (_, { rejectWithValue }) => {
    try {
      const response = await axiosInstance.post("/users/refresh-token");
      console.log("refreshToken->", response.data);
      return response.data;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

const initialState = {
  user: {},
  status: "idle",
  error: null,
};

const userSlice = createSlice({
  name: "user",
  initialState,
  reducers: {},
  extraReducers(builder) {
    builder
      .addCase(logoutUser.pending, (state, action) => {
        state.status = "loading";
      })
      .addCase(logoutUser.fulfilled, (state, action) => {
        state.status = "success";
        console.log("case f logout->", action.payload); // log statement
        state.user = {};
      })
      .addCase(logoutUser.rejected, (state, action) => {
        state.error = action.payload;
      })
      .addCase(refreshUserToken.pending, (state, action) => {
        state.status = "loading";
      })
      .addCase(refreshUserToken.fulfilled, (state, action) => {
        state.status = "success";
        console.log("case f refreshToken->", action.payload); // log statement
      })
      .addCase(refreshUserToken.rejected, (state, action) => {
        state.error = action.payload;
      });
  },
});

export const getUserState = (state) => state.user.status;
export const getUserError = (state) => state.user.error;
export const getUser = (state) => state.user.user;

export const {} = userSlice.actions;

export default userSlice.reducer;

Axios实例代码

const axiosInstance = axios.create({
    baseURL: "http://localhost:8082/api",
    withCredentials: true
});
解决方案

一、直接修正当前重复执行问题

你现在的核心问题是在logoutUser的catch逻辑中再次dispatch了logoutUser,导致该thunk被触发两次。正确的做法是刷新令牌后直接重新调用axios请求,而非再次dispatch thunk。

修改后的logoutUser代码:

export const logoutUser = createAsyncThunk(
  "users/logoutUser",
  async (_, { rejectWithValue, dispatch }) => {
    try {
      const response = await axiosInstance.post("/users/logout");
      return response.data;
    } catch (error) {
      if (error.response.status === 401) {
        try {
          // 用unwrap()获取refreshUserToken的实际返回值,确保刷新成功
          await dispatch(refreshUserToken()).unwrap();
          // 直接调用axios请求,避免重复dispatch thunk
          const retryResponse = await axiosInstance.post("/users/logout");
          return retryResponse.data;
        } catch (refreshError) {
          return rejectWithValue(refreshError.message);
        }
      } else {
        return rejectWithValue(error.message);
      }
    }
  }
);

二、更优的全局令牌刷新方案

单个thunk中处理401会导致重复代码,更合理的方式是在axios拦截器中统一处理令牌刷新,让所有API请求自动处理401重试,无需在每个thunk中重复编写逻辑。

步骤1:改造Axios实例,添加响应拦截器

const axiosInstance = axios.create({
  baseURL: "http://localhost:8082/api",
  withCredentials: true
});

// 添加响应拦截器,统一处理401令牌过期
axiosInstance.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;
    // 仅处理401错误,且标记请求避免重复重试
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      try {
        // 发送刷新令牌请求
        await axiosInstance.post("/users/refresh-token");
        // 重试原请求
        return axiosInstance(originalRequest);
      } catch (refreshError) {
        // 刷新失败(如Refresh Token也过期),可触发退出登录或跳转登录页逻辑
        // dispatch(logoutUser())
        return Promise.reject(refreshError);
      }
    }
    return Promise.reject(error);
  }
);

export default axiosInstance;

步骤2:简化logoutUser thunk

现在无需在thunk中处理401逻辑,代码会更简洁:

export const logoutUser = createAsyncThunk(
  "users/logoutUser",
  async (_, { rejectWithValue }) => {
    try {
      const response = await axiosInstance.post("/users/logout");
      return response.data;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

该方案优势

  • 避免重复代码:所有API请求自动处理401重试,无需在每个asyncThunk中编写相同逻辑
  • 逻辑集中:令牌刷新逻辑统一放在axios拦截器,维护更方便
  • 避免thunk重复触发:直接重试axios请求,不会导致多次dispatch thunk的问题

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 04:57:07