Express+React+TypeScript下JWT鉴权优化及Wrapper异步问题咨询
React+Express+TypeScript鉴权路由问题解决
问题背景
使用Express+React+TypeStack开发时,希望通过每次请求携带token到后端验证用户登录状态。目前每个私有路由都需要编写重复的鉴权逻辑,即便复用了鉴权检查函数,仍要写页面条件渲染代码。尝试用Wrapper组件统一处理,但因React状态更新的异步特性,isAuth初始值为false,导致每次都会跳转到首页。
后端鉴权接口代码
export const isAuth = async (req: Request, res: Response) => { const token = req.get('Authorization')?.split(' ')[1]; if (token) { try { const decodedToken = jwt.verify(token, `${process.env.SECRET_KEY}`); if (decodedToken) res.status(200).json({ isAuth: true }); } catch (e) { res.status(403).json({ isAuth: false }); } } else res.status(403).json({ isAuth: false }); };
原有前端私有路由代码
const AddProductPage = () => { const [isAuth, setIsAuth] = useState<boolean>(false); useEffect(() => { const checkAuth = async () => { const response = await fetch(`${config.backendDomain}/auth/is-auth`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + localStorage.getItem('token'), }, }); const data = await response.json(); setIsAuth(data.isAuth); }; checkAuth(); }, []); return isAuth ? <AddProduct /> : <Navigate to="/" />; };
尝试的Wrapper组件代码
interface WrapperProps { children: React.ReactNode; } const IsAuth: React.FC<WrapperProps> = (props) => { const [isAuth, setIsAuth] = useState(false); useEffect(() => { const checkAuth = async () => { const response = await fetch(`${config.backendDomain}/auth/is-auth`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + localStorage.getItem('token'), }, }); const data = await response.json(); setIsAuth(data.isAuth); }; checkAuth(); }, []); console.log('ISAUTH', isAuth); return isAuth ? <div>{props.children}</div> : <Navigate to="/" />; };
解决方案
1. 增加加载状态避免初始误跳转
React状态更新是异步的,组件挂载时isAuth初始值为false会直接触发跳转。需要添加加载状态,在鉴权请求完成前不执行跳转或显示加载组件:
interface WrapperProps { children: React.ReactNode; } const IsAuth: React.FC<WrapperProps> = (props) => { // 初始设为null表示鉴权未完成 const [isAuth, setIsAuth] = useState<boolean | null>(null); useEffect(() => { const checkAuth = async () => { const token = localStorage.getItem('token'); // 本地无token直接标记为未授权 if (!token) { setIsAuth(false); return; } try { const response = await fetch(`${config.backendDomain}/auth/is-auth`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, }); const data = await response.json(); setIsAuth(data.isAuth); } catch (err) { // 请求失败也标记为未授权 setIsAuth(false); } }; checkAuth(); }, []); // 鉴权未完成时显示加载组件 if (isAuth === null) { return <div>加载中...</div>; // 可替换为自定义Loading组件 } return isAuth ? <>{props.children}</> : <Navigate to="/" />; };
2. 优化后端鉴权逻辑
简化响应逻辑,同时区分不同错误场景:
export const isAuth = async (req: Request, res: Response) => { try { const authHeader = req.get('Authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ isAuth: false, message: '缺少有效token' }); } const token = authHeader.split(' ')[1]; const decodedToken = jwt.verify(token, `${process.env.SECRET_KEY}`); // 返回解码后的用户信息,方便前端复用 res.status(200).json({ isAuth: true, user: decodedToken }); } catch (e) { if (e instanceof jwt.TokenExpiredError) { return res.status(401).json({ isAuth: false, message: 'token已过期' }); } res.status(403).json({ isAuth: false, message: '无效token' }); } };
3. 路由层面统一使用Wrapper
在路由配置中直接用IsAuth包裹所有私有路由,无需每个页面重复编写逻辑:
// 路由配置示例 <Routes> <Route path="/" element={<HomePage />} /> <Route path="/add-product" element={ <IsAuth> <AddProduct /> </IsAuth> } /> {/* 其他私有路由同理 */} </Routes>
4. 可选:封装鉴权请求函数
将鉴权请求封装为独立工具函数,便于全局复用:
// src/utils/auth.ts export const checkAuthStatus = async (): Promise<boolean> => { const token = localStorage.getItem('token'); if (!token) return false; try { const response = await fetch(`${config.backendDomain}/auth/is-auth`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, }); const data = await response.json(); return data.isAuth; } catch (err) { return false; } };
内容的提问来源于stack exchange,提问作者Pentium3k
相关产品推荐
相关产品推荐

