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

React+Redux结合Cookie、LocalStorage管理用户登录登出状态咨询

React登录态与API请求权限管理解决方案

已有实现背景

我在React应用中已完成登录功能开发:

  • 登录后将refresh token和access token存入Redux store,初始store结构如下:
data:{
  isLoggedIn: false,
  userData: null
}
  • 登录成功后会保存userData并将isLoggedIn设为true,同时将access token存入浏览器Cookie(有效期1小时)
  • 已通过redux-persist将store持久化到localStorage中
  • 当前路由判断代码:
const userInfo = useSelector((state) => state.userReducer);
const isUserLoggedIn = userInfo.userData.isLoggedIn;

<Route
  path="/"
  element={isUserLoggedIn ? <DashBoardLayout /> : <LoginPage />}
>
  {isUserLoggedIn ? (
    <Route index element={<Overview />} />
  ) : (
    <Route index element={<LoginPage />} />
  )}
</Route>

需要解决的问题

  • 为所有API请求创建中间件,验证浏览器Cookie,若Cookie过期则调用/api/refreshToken接口生成新token并更新Cookie;
  • 检查refresh token(有效期24小时),若其过期或Cookie为空则自动登出用户;
  • 解决登录一段时间后调用API出现未授权错误的问题,需在API请求前更新Cookie;
  • 实现路由权限控制,限制未登录用户访问受限路由;
  • 根据Cookie和refresh token的验证结果同步管理Redux store状态。

解决方案

1. 实现API请求中间件(以Axios为例)

通过Axios拦截器实现请求前的token校验与自动刷新:

import axios from 'axios';
import { logoutUser, updateAccessToken } from './redux/userActions';
import store from './redux/store';

const api = axios.create({ baseURL: '/api' });

// 请求拦截器:提前校验token并刷新
api.interceptors.request.use(
  async (config) => {
    // 从Cookie获取access token
    const accessToken = document.cookie.split('; ').find(row => row.startsWith('accessToken='))?.split('=')[1];
    
    if (accessToken) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }

    // 校验access token是否过期
    const isTokenExpired = () => {
      if (!accessToken) return true;
      const decoded = JSON.parse(atob(accessToken.split('.')[1]));
      return decoded.exp * 1000 < Date.now();
    };

    if (isTokenExpired()) {
      try {
        const { userData } = store.getState().userReducer.data;
        const refreshToken = userData?.refreshToken;

        if (!refreshToken) throw new Error('无可用刷新令牌');

        // 调用刷新接口
        const res = await axios.post('/api/refreshToken', { refreshToken });
        const newAccessToken = res.data.accessToken;

        // 更新Cookie(1小时有效期)
        document.cookie = `accessToken=${newAccessToken}; max-age=3600; path=/; secure; SameSite=Strict`;
        // 更新请求头与Redux store
        config.headers.Authorization = `Bearer ${newAccessToken}`;
        store.dispatch(updateAccessToken(newAccessToken));
      } catch (err) {
        store.dispatch(logoutUser());
        return Promise.reject(err);
      }
    }
    return config;
  },
  (err) => Promise.reject(err)
);

// 响应拦截器:处理401未授权错误
api.interceptors.response.use(
  (res) => res,
  async (err) => {
    if (err.response?.status === 401) {
      try {
        const { userData } = store.getState().userReducer.data;
        const res = await axios.post('/api/refreshToken', { refreshToken: userData.refreshToken });
        const newAccessToken = res.data.accessToken;

        document.cookie = `accessToken=${newAccessToken}; max-age=3600; path=/; secure; SameSite=Strict`;
        store.dispatch(updateAccessToken(newAccessToken));

        // 重发原请求
        err.config.headers.Authorization = `Bearer ${newAccessToken}`;
        return api(err.config);
      } catch (refreshErr) {
        store.dispatch(logoutUser());
        return Promise.reject(refreshErr);
      }
    }
    return Promise.reject(err);
  }
);

export default api;

2. 校验Refresh Token有效性与自动登出

在应用初始化或路由守卫中添加refresh token校验逻辑:

import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { logoutUser } from './redux/userActions';

const App = () => {
  const dispatch = useDispatch();
  const { userData } = useSelector((state) => state.userReducer.data);

  useEffect(() => {
    const checkRefreshToken = () => {
      if (!userData?.refreshToken) {
        dispatch(logoutUser());
        return;
      }

      // 解码refresh token校验过期时间
      const decoded = JSON.parse(atob(userData.refreshToken.split('.')[1]));
      if (decoded.exp * 1000 < Date.now()) {
        dispatch(logoutUser());
      }
    };

    checkRefreshToken();
    // 每小时定时校验一次
    const interval = setInterval(checkRefreshToken, 3600000);
    return () => clearInterval(interval);
  }, [dispatch, userData]);

  // 组件其他逻辑...
};

3. 解决API未授权错误

通过上述Axios拦截器的两层校验:

  • 请求前主动检查access token是否过期,提前刷新
  • 请求返回401时自动尝试刷新token并重发请求
    完全覆盖登录后一段时间API未授权的场景

4. 路由权限控制(React Router v6)

封装PrivateRoute组件实现权限拦截:

import { Navigate, Outlet } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { useEffect } from 'react';
import { logoutUser } from './redux/userActions';

const PrivateRoute = () => {
  const dispatch = useDispatch();
  const { isLoggedIn, userData } = useSelector((state) => state.userReducer.data);

  useEffect(() => {
    if (isLoggedIn) {
      const decoded = JSON.parse(atob(userData.refreshToken.split('.')[1]));
      if (decoded.exp * 1000 < Date.now()) {
        dispatch(logoutUser());
      }
    }
  }, [dispatch, isLoggedIn, userData]);

  return isLoggedIn ? <Outlet /> : <Navigate to="/login" replace />;
};

// 路由配置示例
const RoutesConfig = () => {
  return (
    <Routes>
      <Route path="/login" element={<LoginPage />} />
      <Route element={<PrivateRoute />}>
        <Route path="/" element={<DashBoardLayout />}>
          <Route index element={<Overview />} />
          {/* 其他受限路由 */}
        </Route>
      </Route>
      <Route path="*" element={<Navigate to="/" replace />} />
    </Routes>
  );
};

5. 同步Redux Store与Cookie、Refresh Token状态

  • 登录成功:更新store的isLoggedIn为true,保存userData(含refresh token),同时设置access token到Cookie
  • 刷新token成功:更新Cookie中的access token,同步更新store中的access token(若store存储)
  • 登出操作:清空Cookie中的access token,重置store状态,清除localStorage中的持久化缓存
  • 应用初始化:读取持久化store后,立即校验refresh token,若过期则触发登出同步状态

登出action示例:

// userActions.js
export const logoutUser = () => (dispatch) => {
  // 清空Cookie
  document.cookie = 'accessToken=; max-age=0; path=/';
  // 重置store
  dispatch({
    type: 'LOGOUT_USER',
    payload: { isLoggedIn: false, userData: null }
  });
  // 清除redux-persist缓存
  localStorage.removeItem('persist:root');
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 17:35:43