生产环境下React调用Express API时Token/Cookie未定义问题
React + Express 跨域认证生产环境异常问题
问题现象
- 后端部署在Heroku平台,前端部署在Netlify平台,本地开发阶段功能完全正常,生产环境出现认证异常
- 用户可正常完成登录操作,但无法访问个人资料页面,接口返回提示token/cookie未定义
- 登录成功时Cookie中存在token,但点击页面链接或刷新页面后token消失,导致所有需要用户认证的功能无法使用
相关代码片段
后端登录接口实现
const loginUser = async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { email, password } = req.body; try { // 检查用户是否存在 const user = await User.findOne({ email }); if (!user) { return res.status(400).json({ msg: "Invalid credentials" }); } // 验证密码是否匹配 const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { return res.status(400).json({ msg: "Invalid credentials" }); } // 创建JWT payload const payload = { user: { id: user.id, role: user.role, }, }; // 签发JWT jwt.sign( payload, process.env.JWT_SECRET, { expiresIn: "2h" }, // 有效期2小时 (err, token) => { if (err) throw err; // 设置HTTP-only Cookie res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", // 生产环境启用secure sameSite: "strict", // 防止CSRF maxAge: 2 * 60 * 60 * 1000, // 2小时有效期 }); // 返回用户数据和token res.status(200).json({ user: { id: user.id, email: user.email, first_name: user.first_name, last_name: user.last_name, role: user.role, }, token: token, }); } ); } catch (err) { console.error(err.message); res.status(500).json({ msg: "Server error" }); } };
后端路由配置
router.post( "/api/login", [ check("email", "Please include a valid email").isEmail(), check("password", "Password is required").exists(), ], userController.loginUser );
前端登录组件
import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import axios from "axios"; const UserLoginForm = ({ setUser }) => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const navigate = useNavigate(); const handleSubmit = async (e) => { e.preventDefault(); try { const response = await axios.post( "https://app.herokuapp.com/users/api/login", { email, password }, { withCredentials: true } ); setUser(response.data.user); console.log(response.data); localStorage.setItem("user", JSON.stringify(response.data.user)); navigate("/"); } catch (err) { setError(err.response?.data?.message || "An error occurred during login"); } }; return ( <div className="min-h-screen bg-gray-100 flex flex-col justify-center py-12 sm:px-6 lg:px-8"> <div className="sm:mx-auto sm:w-full sm:max-w-md"> <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900"> Sign in to your account </h2> </div> <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> <div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10"> <form className="space-y-6" onSubmit={handleSubmit}> <div> <label htmlFor="email" className="block text-sm font-medium text-gray-700" > Email address </label> <div className="mt-1"> <input id="email" name="email" type="email" autoComplete="email" required value={email} onChange={(e) => setEmail(e.target.value)} className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" /> </div> </div> <div> <label htmlFor="password" className="block text-sm font-medium text-gray-700" > Password </label> <div className="mt-1"> <input id="password" name="password" type="password" autoComplete="current-password" required value={password} onChange={(e) => setPassword(e.target.value)} className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" /> </div> </div> <div> <button type="submit" className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" > Sign in </button> </div> </form> {error && ( <div className="mt-4 text-center text-red-600">{error}</div> )} </div> </div> </div> ); }; export default UserLoginForm;
认证中间件
const jwt = require("jsonwebtoken"); const User = require("../models/User"); const Salon = require("../models/Salon"); const authMiddleware = async (req, res, next) => { console.log(req.cookies, "this is cookies"); try { // 从Cookie或请求头获取token const token = req.cookies.token || req.header("x-auth-token"); // 无token时设置未认证状态 if (!token) { req.user = null; req.salon = null; res.locals.authenticatedEntityId = null; return next(); } // 验证token const decoded = jwt.verify(token, process.env.JWT_SECRET); if (decoded.user) { // 处理用户认证 const user = await User.findById(decoded.user.id).select("-password"); if (!user) throw new Error("User not found"); req.user = user; req.salon = null; res.locals.authenticatedEntityId = user._id; } else if (decoded.salon) { // 处理商家认证 const salon = await Salon.findById(decoded.salon.id).select("-password"); if (!salon) throw new Error("Salon not found"); req.salon = salon; req.user = null; res.locals.authenticatedEntityId = salon._id; } else { // 无效token req.user = null; req.salon = null; res.locals.authenticatedEntityId = null; } next(); } catch (error) { console.error("Auth Middleware Error:", error); req.user = null; req.salon = null; res.locals.authenticatedEntityId = null; next(); } }; module.exports = authMiddleware;
后端app.js配置
app.use( cors({ origin: "https://client.netlify.app", // 允许Netlify前端请求 credentials: true, // 允许携带凭证 methods: ["GET", "POST", "PUT", "DELETE"], // 允许的请求方法 }) ); // 创建HTTP服务器并配置Socket.IO const server = http.createServer(app); const io = socketIo(server, { cors: { origin: "https://client.netlify.app", methods: ["GET", "POST", "PUT", "DELETE"], credentials: true, }, }); app.set("io", io); // Socket.IO事件监听 io.on("connection", (socket) => { console.log("New client connected"); socket.on("newComment", (data) => { io.emit("updateComments", data); }); socket.on("newReply", (data) => { io.emit("updateReplies", data); }); socket.on("disconnect", () => { console.log("Client disconnected"); }); }); // MongoDB连接 mongoose .connect(process.env.MONGO_URI, {}) .then(() => console.log("MongoDB connected successfully")) .catch((err) => console.error("MongoDB connection error:", err)); // 视图引擎配置 app.set("views", path.join(__dirname, "views")); app.set("view engine", "ejs"); app.use("/webhook", bodyParser.raw({ type: "application/json" })); app.use(logger("dev")); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, "public"))); // Session配置 app.use( session({ secret: process.env.JWT_SECRET, resave: false, saveUninitialized: false, store: MongoStore.create({ mongoUrl: process.env.MONGO_URI }), cookie: { maxAge: 1000 * 60 * 60 * 24, // 1天有效期 httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict", }, }) );
问题原因及修复方案
核心问题
- SameSite属性不兼容跨域场景:生产环境下前后端分属不同域名,
sameSite: "strict"会阻止浏览器在跨域请求中携带Cookie,导致跳转/刷新后后端无法获取token - Cookie未配置Domain属性:跨域场景下未指定Domain,浏览器仅在当前域名下携带Cookie,后续请求不会传递到后端
- 前端axios未全局配置凭证携带:仅登录请求设置了
withCredentials: true,其他API请求未配置,导致后续请求不携带Cookie - Session与JWT Cookie冲突:同时启用Session和JWT Cookie认证,可能导致Cookie存储或传递异常
修复步骤
1. 修改后端Cookie配置
调整登录接口中res.cookie的参数,适配跨域场景:
res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: process.env.NODE_ENV === "production" ? "none" : "strict", // 生产环境用none兼容跨域 domain: process.env.NODE_ENV === "production" ? ".herokuapp.com" : undefined, // 配置Heroku根域名 maxAge: 2 * 60 * 60 * 1000, });
注意:
sameSite: "none"必须配合secure: true使用,仅HTTPS环境生效
2. 全局配置axios携带凭证
在前端项目中创建axios全局实例,统一配置withCredentials:
// src/utils/axios.js import axios from 'axios'; const api = axios.create({ baseURL: "https://app.herokuapp.com", withCredentials: true, // 全局开启凭证携带 }); export default api;
后续所有API请求均使用该实例,替代直接调用axios
3. 移除Session中间件(可选)
当前使用JWT Cookie认证,Session配置无必要,可注释或移除:
// 注释以下代码 // app.use( // session({ // secret: process.env.JWT_SECRET, // resave: false, // saveUninitialized: false, // store: MongoStore.create({ mongoUrl: process.env.MONGO_URI }), // cookie: { // maxAge: 1000 * 60 * 60 * 24, // httpOnly: true, // secure: process.env.NODE_ENV === "production", // sameSite: "strict", // }, // }) // );
4. 添加页面刷新时的认证校验
在App组件中添加刷新时的用户状态校验,避免localStorage数据与后端状态不一致:
// App.js import { useEffect } from 'react'; import api from './utils/axios'; function App({ setUser }) { useEffect(() => { const checkAuthStatus = async () => { const savedUser = localStorage.getItem("user"); if (savedUser) { try { const res = await api.get("/users/api/me"); // 后端需实现获取当前用户接口 setUser(res.data.user); } catch (err) { localStorage.removeItem("user"); setUser(null); } } }; checkAuthStatus(); }, [setUser]); // ... 其他组件逻辑 }
内容的提问来源于stack exchange,提问作者Chukwuma Kingsley
相关产品推荐
相关产品推荐

