React+Node.js基于JWT的安全认证流程实现咨询
完整认证流程实现方案
核心原则:绝对不要用localStorage存JWT
既然后端已经返回httpOnly: true的Cookie,就完全没必要把JWT存localStorage——localStorage易遭XSS攻击窃取,而httpOnly Cookie由浏览器托管,前端JS无法读取,安全性高一个量级。
后端调整要点
以Express为例,登录接口的Cookie配置必须覆盖这些细节:
- 加
sameSite: 'strict'(同域场景),防范CSRF攻击 - 明确
domain和path,确保前端请求能自动带上Cookie - 生产环境强制
secure: true,仅通过HTTPS传输Cookie
示例代码:
// 登录接口返回Cookie app.post('/login', async (req, res) => { const { username, password } = req.body; // 用户验证逻辑... const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' }); res.cookie('auth_token', token, { httpOnly: true, sameSite: 'strict', domain: 'localhost', path: '/', maxAge: 3600000 // 和token过期时间保持一致 }); res.json({ success: true, message: '登录成功' }); }); // Token验证中间件 const authenticate = (req, res, next) => { const token = req.cookies.auth_token; if (!token) return res.status(401).json({ success: false, message: '未授权' }); try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; next(); } catch (err) { res.clearCookie('auth_token'); // 清除无效/过期Cookie return res.status(401).json({ success: false, message: 'Token无效' }); } }; // 受保护接口示例 app.get('/api/user', authenticate, (req, res) => { res.json({ userId: req.user.userId, username: user.username }); }); // 登出接口:清除Cookie app.post('/logout', (req, res) => { res.clearCookie('auth_token', { httpOnly: true, sameSite: 'strict', domain: 'localhost', path: '/' }); res.json({ success: true, message: '已登出' }); });
前端React实现步骤
1. 用Context维护认证状态
通过调用后端验证接口(而非存token)判断登录状态,所有请求必须加credentials: 'include'才能携带httpOnly Cookie:
// AuthContext.js import { createContext, useContext, useState, useEffect } from 'react'; const AuthContext = createContext(); export const AuthProvider = ({ children }) => { const [isAuthenticated, setIsAuthenticated] = useState(false); const [loading, setLoading] = useState(true); // 初始化时检查登录状态 useEffect(() => { const checkAuth = async () => { try { const res = await fetch('http://localhost:4000/api/user', { credentials: 'include' }); setIsAuthenticated(res.ok); } catch (err) { setIsAuthenticated(false); } finally { setLoading(false); } }; checkAuth(); }, []); const login = () => setIsAuthenticated(true); const logout = async () => { await fetch('http://localhost:4000/logout', { method: 'POST', credentials: 'include' }); setIsAuthenticated(false); }; return ( <AuthContext.Provider value={{ isAuthenticated, loading, login, logout }}> {children} </AuthContext.Provider> ); }; export const useAuth = () => useContext(AuthContext);
2. 实现真实的ProtectedRoute组件
基于Context的认证状态做路由拦截,替换模拟状态:
// ProtectedRoute.js import { Navigate } from 'react-router-dom'; import { useAuth } from './AuthContext'; export const ProtectedRoute = ({ children }) => { const { isAuthenticated, loading } = useAuth(); if (loading) return <div>加载中...</div>; return isAuthenticated ? children : <Navigate to="/login" />; };
3. 登录组件逻辑
登录成功后直接更新Context状态,无需存储任何token:
// Login.js import { useState } from 'react'; import { useAuth } from './AuthContext'; import { useNavigate } from 'react-router-dom'; export const Login = () => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const { login } = useAuth(); const navigate = useNavigate(); const handleSubmit = async (e) => { e.preventDefault(); try { const res = await fetch('http://localhost:4000/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), credentials: 'include' }); if (res.ok) { login(); navigate('/dashboard'); } else { alert('登录失败'); } } catch (err) { alert('网络错误'); } }; return ( <form onSubmit={handleSubmit}> <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} placeholder="用户名" required /> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="密码" required /> <button type="submit">登录</button> </form> ); };
跨域配置注意
因为前后端是不同端口,Express必须配置CORS允许携带Cookie:
const cors = require('cors'); app.use(cors({ origin: 'http://localhost:3000', credentials: true // 必须开启,否则Cookie不会被发送 }));
总结
- 全程依赖httpOnly Cookie,彻底放弃localStorage,规避XSS风险
- 前端通过后端验证接口判断登录状态,而非自行存储token
- 所有涉及认证的请求必须加
credentials: 'include' - ProtectedRoute基于真实的后端认证结果做拦截
内容的提问来源于stack exchange,提问作者Keith Claclaws
相关产品推荐
相关产品推荐

