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

Redux持久化存储异常:两次刷新后用户自动登出求助

问题分析与修复方案

核心问题点

  1. Redux Persist配置重复持久化
    你同时对authReducer和整个rootReducer应用了persistReducer,这会导致持久化逻辑冲突,引发数据存储/读取异常,是第二次刷新登出的主要原因之一。

  2. Axios拦截器依赖组件快照状态,未实时获取最新token
    自定义hookuseAxiosInstance中通过useSelector获取的token是组件初始化时的快照,不会随Redux状态更新,导致拦截器始终使用旧token,触发错误的过期判断或登出逻辑。

  3. 拦截器重复注册
    每次调用useAxiosInstance都会创建新的axios实例并添加请求拦截器,导致拦截逻辑重复执行,加剧状态混乱。

  4. Persist加载完成前触发登出
    页面刷新后,Redux Persist需要从localStorage恢复状态,若组件先挂载,此时token为初始值(null),拦截器会直接触发logOut并清除localStorage的refresh token,导致第二次刷新时无有效凭证。

  5. Refresh Token存储不同步
    你将refresh token单独存在localStorage,未纳入Redux持久化体系,导致Redux状态与localStorage数据不一致,状态恢复时丢失关键凭证。


修复步骤

1. 修正Redux Persist配置

去掉对authReducer单独的persistReducer包装,只对整个rootReducer做一次持久化,或通过whitelist指定仅持久化auth模块:

// app/store.js
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import { apiSlice } from "./api/apiSlice";
import { persistStore, persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
import authReducer from "../features/auth/authSlice";
import thunk from "redux-thunk";
import formReducer from "../features/modal/formReducer";
import modalReducer from "../features/modal/modalReducer";
import elementReducer from "../features/elements/elementReducer";
import editPageReducer from "../features/elements/editPageReducer";

const persistConfig = {
  key: "root",
  storage,
  whitelist: ["auth"], // 仅持久化auth模块,其他模块不保存
};

const rootReducer = combineReducers({
  auth: authReducer, // 不再单独包装persistReducer
  formState: formReducer,
  modalState: modalReducer,
  elementState: elementReducer,
  editPageState: editPageReducer,
});

const persistedReducer = persistReducer(persistConfig, rootReducer);

export const store = configureStore({
  reducer: persistedReducer,
  middleware: [thunk, apiSlice.middleware],
  devTools: true,
});

export const persistor = persistStore(store);

2. 重构Axios实例为单例,直接访问Redux Store

避免用React Hook管理axios实例,改用单例模式,直接从Redux Store获取最新状态,确保拦截器始终使用最新token:

// utils/axiosInstance.js
import axios from "axios";
import jwt_decode from "jwt-decode";
import dayjs from "dayjs";
import store from "../app/store";
import { setCredentials, logOut } from "../features/auth/authSlice";

const baseURL = "http://127.0.0.1:8000/api/";

// 创建单例axios实例
const axiosInstance = axios.create({
  baseURL,
});

axiosInstance.interceptors.request.use(async (req) => {
  // 直接从Store获取最新的auth状态
  const { auth } = store.getState();
  const { token, refreshToken, user } = auth;

  if (!token) {
    // 若token不存在,先返回请求,等待Persist加载完成或用户重新登录
    return req;
  }

  try {
    // 判断token是否过期
    const isExpired = dayjs.unix(jwt_decode(token).exp).diff(dayjs()) < 1;

    if (!isExpired) {
      req.headers.Authorization = `Bearer ${token}`;
      return req;
    }

    // 若refresh token不存在,直接登出
    if (!refreshToken) {
      store.dispatch(logOut());
      return req;
    }

    // 请求新的access token
    const response = await axios.post(`${baseURL}token/refresh/`, {
      refresh: refreshToken,
    });

    const newAccessToken = response.data.access;
    const newRefreshToken = response.data.refresh;

    // 更新Redux状态
    store.dispatch(setCredentials({
      access: newAccessToken,
      refresh: newRefreshToken,
      user
    }));

    // 更新当前请求的Authorization头
    req.headers.Authorization = `Bearer ${newAccessToken}`;
    return req;
  } catch (error) {
    // 刷新失败,清除状态并登出
    store.dispatch(logOut());
    throw error;
  }
});

export default axiosInstance;

3. 更新Auth Slice,纳入Refresh Token管理

确保refresh token存储在Redux状态中,随persist一起持久化:

// features/auth/authSlice.js
import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  user: null,
  token: null,
  refreshToken: null,
};

const authSlice = createSlice({
  name: "auth",
  initialState,
  reducers: {
    setCredentials: (state, action) => {
      const { user, access, refresh } = action.payload;
      state.user = user;
      state.token = access;
      state.refreshToken = refresh;
    },
    logOut: (state) => {
      state.user = null;
      state.token = null;
      state.refreshToken = null;
    },
  },
});

export const { setCredentials, logOut } = authSlice.actions;
export const selectCurrentToken = (state) => state.auth.token;
export default authSlice.reducer;

4. 确保应用等待Persist加载完成

在入口文件用PersistGate包裹App组件,等待Redux状态从localStorage恢复完成后再渲染:

// index.js
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { store, persistor } from "./app/store";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <PersistGate loading={null} persistor={persistor}>
      <App />
    </PersistGate>
  </Provider>
);

5. 调整登录逻辑,同步存储Refresh Token

登录时将后端返回的refresh token存入Redux,不再单独操作localStorage:

// 示例登录组件
import { useDispatch } from "react-redux";
import axiosInstance from "../utils/axiosInstance";
import { setCredentials } from "../features/auth/authSlice";

const Login = () => {
  const dispatch = useDispatch();

  const handleLogin = async (formData) => {
    try {
      const response = await axiosInstance.post("/token/", formData);
      // 假设后端返回user、access、refresh字段
      const { user, access, refresh } = response.data;
      dispatch(setCredentials({ user, access, refresh }));
    } catch (error) {
      // 处理登录错误
      console.error("登录失败:", error);
    }
  };

  return (
    // 登录表单
  );
};

export default Login;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 18:34:59