Redux持久化存储异常:两次刷新后用户自动登出求助
核心问题点
Redux Persist配置重复持久化
你同时对authReducer和整个rootReducer应用了persistReducer,这会导致持久化逻辑冲突,引发数据存储/读取异常,是第二次刷新登出的主要原因之一。Axios拦截器依赖组件快照状态,未实时获取最新token
自定义hookuseAxiosInstance中通过useSelector获取的token是组件初始化时的快照,不会随Redux状态更新,导致拦截器始终使用旧token,触发错误的过期判断或登出逻辑。拦截器重复注册
每次调用useAxiosInstance都会创建新的axios实例并添加请求拦截器,导致拦截逻辑重复执行,加剧状态混乱。Persist加载完成前触发登出
页面刷新后,Redux Persist需要从localStorage恢复状态,若组件先挂载,此时token为初始值(null),拦截器会直接触发logOut并清除localStorage的refresh token,导致第二次刷新时无有效凭证。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

