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

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不会被发送
}));

总结

  1. 全程依赖httpOnly Cookie,彻底放弃localStorage,规避XSS风险
  2. 前端通过后端验证接口判断登录状态,而非自行存储token
  3. 所有涉及认证的请求必须加credentials: 'include'
  4. ProtectedRoute基于真实的后端认证结果做拦截

内容的提问来源于stack exchange,提问作者Keith Claclaws

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 04:15:22