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
相关产品推荐
相关产品推荐

