React前端如何处理JWT过期导致的未授权(无Token)问题?
解决方案:JWT过期自动登出与状态同步问题修复
第一步:修复后端错误状态码返回问题
你的后端存在两处错误导致前端无法正确捕获401状态:
protect函数中else块的代码顺序错误,先抛出错误再设置状态码,导致状态码不生效createPost控制器的try-catch覆盖了protect抛出的401错误,强制返回500状态
修改protect函数:
const protect = asyncHandler(async (req, res, next) => { let token = req.cookies.jwt; if (token) { try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = await User.findById(decoded.userId).select('-password'); next(); } catch (error) { res.status(401); throw new Error('Not authorized, token failed'); } } else { res.status(401); // 先设置状态码再抛出错误 throw new Error('Not authorized, no token'); } });
修改createPost控制器:
移除多余的try-catch(asyncHandler已自动处理错误传递),同时补全await关键字:
const createPost = asyncHandler(async (req, res) => { const { type, text, images } = req.body; const newPost = new Post({ type, text, images, user: req.user._id, }); const post = await newPost.save(); // 补全await,返回实际文档而非Promise res.status(200).json(post); });
第二步:前端全局拦截401错误(RTK Query)
不要在单个请求中处理401,通过RTK Query的全局拦截器统一处理所有请求的权限错误:
自定义带权限校验的baseQuery:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { logout } from '../features/auth/authSlice'; import toast from 'react-hot-toast'; const baseQuery = fetchBaseQuery({ baseUrl: '/api', credentials: 'include', // 确保携带cookie }); const baseQueryWithAuth = async (args, api, extraOptions) => { const result = await baseQuery(args, api, extraOptions); // 捕获401状态,触发登出逻辑 if (result.error && result.error.status === 401) { api.dispatch(logout()); toast.error('登录已过期,请重新登录'); } return result; }; export const apiSlice = createApi({ reducerPath: 'api', baseQuery: baseQueryWithAuth, tagTypes: ['Posts'], endpoints: (builder) => ({ // 你的接口定义(createPost、getMyPosts等) }), });
第三步:实现登出与自动重定向逻辑
完善authSlice的logout action:
import { createSlice } from '@reduxjs/toolkit'; const initialState = { user: null, }; const authSlice = createSlice({ name: 'auth', initialState, reducers: { setUser: (state, action) => { state.user = action.payload; }, logout: (state) => { state.user = null; // 手动设置cookie过期,触发浏览器删除httpOnly cookie document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; }, }, }); export const { setUser, logout } = authSlice.actions; export default authSlice.reducer;
全局监听用户状态,自动重定向:
在App.js或根组件中添加状态监听:
import { useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import { useEffect } from 'react'; function App() { const user = useSelector(state => state.auth.user); const navigate = useNavigate(); useEffect(() => { if (!user) { const currentPath = window.location.pathname; // 避免登录页/注册页重复重定向 if (currentPath !== '/login' && currentPath !== '/register') { navigate('/login'); } } }, [user, navigate]); // ...其他组件渲染逻辑 } export default App;
第四步:简化createPost的错误处理
有了全局拦截后,单个请求只需处理非401的业务错误:
const postSubmit = async () => { if (images && images.length) { try { const postImages = images.map((img) => dataURItoBlob(img)); const path = `tyt/${username}/post_images`; let formData = new FormData(); formData.append('path', path); postImages.forEach((image) => formData.append('file', image)); const response = await uploadImages(formData, path); // 使用unwrap()获取成功数据或抛出错误 await createPost({ type: null, text: text, images: response, }).unwrap(); refetch(); setPostVisible(false); setText(''); setImages([]); } catch (error) { toast.error(error?.data?.message || '发布失败'); } } else if (text) { try { await createPost({ type: null, text: text, images: null, }).unwrap(); refetch(); setPostVisible(false); setText(''); } catch (error) { toast.error(error?.data?.message || '发布失败'); } } };
修改完成后,当JWT过期时:
- 后端会正确返回401状态码
- 前端全局拦截器捕获后触发登出,清除Redux用户状态
- 根组件监听状态变化,自动重定向到登录页并弹出提示
内容的提问来源于stack exchange,提问作者Stephen Scott Moore
相关产品推荐
相关产品推荐

