Express+PassportJS会话异常:Profile请求重复创建新会话
后端使用Express(运行在5500端口),通过PassportJS实现用户认证,用session追踪登录状态;前端采用React框架(运行在3000端口)。
目前调用/api/login的POST请求可成功登录用户,返回200状态码并创建包含Passport用户对象的会话,跳转至Profile页面后,调用/api/profile的GET请求获取用户数据时,每次都会创建新会话,覆盖登录时生成的会话,导致认证状态丢失,提示用户未认证。
后端代码
server.js
const express = require("express"); const MongoStore = require("connect-mongo"); const indexRouter = require("./routes/index"); const app = express(); const cors = require("cors"); const bodyparser = require("body-parser"); const mongoose = require("mongoose"); const dotenv = require("dotenv"); const passport = require("./Authentication/passportConfig"); const session = require("express-session"); const cookieParser = require("cookie-parser"); dotenv.config({ path: "../app/Private/.env" }); let port = process.env.PORT; app.use(bodyparser.json()); app.use(bodyparser.urlencoded({ extended: true })); app.use( cors({ origin: "http://localhost:3000", methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], credentials: true, }) ); app.use(cookieParser("secret")); app.use( session({ secret: "secret", store: MongoStore.create({ mongoUrl: process.env.ADMIN_URI }), resave: false, saveUninitialized: false, cookie: { maxAge: 30 * 24 * 60 * 60 * 1000, httpOnly: true }, }) ); app.use(passport.initialize()); app.use(passport.session()); //Database connection process mongoose .connect(process.env.ADMIN_URI, { useNewUrlParser: true, useUnifiedTopology: true, }) .then(() => console.log("connected to database")) .catch(console.error); //Route creation app.use("/api", indexRouter); app.listen(port, () => console.log("Server connected and running: " + port));
路由代码(indexRouter)
const express = require("express"); const router = express.Router(); const passport = require("../Authentication/passportConfig"); const dotenv = require("dotenv"); dotenv.config({ path: "../app/Private/.env" }); const nodemailer = require("nodemailer"); const User = require("../model/user"); const hash = require("../Authentication/passwordHash"); const validator = require("node-email-validation"); const Appointment = require("../model/appointment"); router.post("/login", passport.authenticate("local"), (req, res) => { var time = new Date(Date.now()); console.log( "Session ID before @ /login route: " + time.toLocaleTimeString() + ": " + req.sessionID ); if (req.user) { console.log("Session ID after @ /login route: " + req.sessionID); console.log("User is logged in: " + req.user.username); console.log("User is Authenticated: " + req.isAuthenticated()); res.status(200).json({ message: "Login successful" }); } else { res.send(400).json({ message: "User not found" }); } }); router.get("/profile", function (req, res) { var time = new Date(Date.now()); console.log( "Session ID @ /profile route: " + time.toLocaleTimeString() + ": " + req.sessionID ); if (req.isAuthenticated()) { console.log(req.isAuthenticated()); console.log("user: " + req.user.username); return res.status(200).json(req.user); num = num + 1; } else { console.log("else Statement"); return res.status(302).json({ message: "User not found" }); } });
会话日志输出
Session ID before @ /login route: 5:41:07 PM: 8Z1RebDKynOESWoQ5ecA3EecOojmthL9 Session ID after @ /login route: 8Z1RebDKynOESWoQ5ecA3EecOojmthL9 User is logged in: alex@s3v.us User is Authenticated: true Session ID @ /profile route: 5:41:09 PM: -peiT761fG_ZY9gLgWFpqeUE6hTZmRQV else Statement Session ID @ /profile route: 5:41:09 PM: kgXFngZMamCqh4mapynMQQN7cgL9Er-1 else Statement Session ID @ /profile route: 5:41:09 PM: zCZTtlsK-g0MSvo9j5ZbAAs35vaXtsnO else Statement Session ID @ /profile route: 5:41:09 PM: Oq8J-s08m66P5JuuTO1RI5ZIy8oOeXRD else Statement
前端代码
Login页面调用代码
const config = { headers: { "Access-Control-Allow-Origin": "http://localhost:3000", "Access-Control-Allow-Methods": "GET,PUT,POST,DELETE,PATCH,OPTIONS", "Access-Control-Allow-Credentials": "true", }, }; const isLoggedIn = () => { axios .get("http://localhost:5500/api/profile", config) .then((res) => { console.log("Is user logged in status: " + res.status); console.log(res.data); if (res.status === 200) { window.location.href = "/MyProfile"; } }) .catch((err) => { console.log("err" + err); }); }; /* on Submit function for the login page */ const onSubmitLogIn = (e) => { e.preventDefault(); const user = { username: username.toString().toLowerCase(), password: password, }; axios .post("http://localhost:5500/api/login", user, config) .then((res) => { console.log(res.data); if (res.status === 200) { setSuccessMessage(true); setTimeout(() => { window.location.href = "/MyProfile"; }, 2000); } }) .catch((err) => { setErrormessage(true); }); };
Profile页面请求代码
useEffect(() => { fetchUserData(); }, []); const fetchUserData = () => { axios .get("http://localhost:5500/api/profile", config) .then((res) => { console.log("res: " + res); console.log("User: " + res.data); setUserData(res.data); }) .catch((err) => { console.log(err); console.log(err.response); }); };
问题根源与修复方案
问题根源
前端跨域请求时未携带会话Cookie,后端无法识别已登录的会话,因此每次请求/api/profile都会创建新会话ID,导致认证状态丢失。
修复步骤
修正前端axios请求配置
你当前的config设置了后端才需要返回的Access-Control-*响应头,属于无效配置。需要添加withCredentials: true,让axios在跨域请求时自动携带Cookie:const config = { withCredentials: true, };优化后端session的Cookie配置
在session配置中添加sameSite: "lax",确保跨域场景下Cookie能正常传递(本地开发环境用lax即可,生产环境如果是HTTPS可以用none并配合secure: true):app.use( session({ secret: "secret", store: MongoStore.create({ mongoUrl: process.env.ADMIN_URI }), resave: false, saveUninitialized: false, cookie: { maxAge: 30 * 24 * 60 * 60 * 1000, httpOnly: true, sameSite: "lax" // 新增配置 }, }) );验证效果
登录成功后,检查浏览器的Cookie存储(开发者工具 -> Application -> Cookies),确认存在connect.sidCookie;访问Profile页面时,后端日志的req.sessionID应与登录时一致,此时req.isAuthenticated()会返回true,可正常获取用户数据。
内容的提问来源于stack exchange,提问作者Vibhore Sagar

