跨域应用中Express Session模块无法保存会话Cookie问题排查
问题描述
本地开发环境下,Node.js服务运行在http://localhost:3002,Vite托管的客户端运行在http://localhost:5173。尝试使用express-session实现用户认证,服务器向客户端发送会话Cookie,但每次请求都会生成新会话,Cookie未正常传递。
服务器代码
this.#app.use(cookieParser()); this.#app.use(cors({ origin: WEB_CLIENT_URL, credentials: true, methods:'GET, POST', allowedHeaders:'Origin, X-Requested-With, Content-Type, Accept, authorization' })); var sess = { secret: 'keyboard cat', saveUninitialized: false, resave: false, cookie: { secret: 'yourSecret', secure: process.env.NODE_ENV === 'production', httpOnly: process.env.NODE_ENV === 'production', sameSite: "none" as "none", maxAge: 24 * 60 * 60 * 1000, // 24 hours domain: undefined//'localhost:3002' }, } if (process.env.NODE_ENV === 'production') { this.#app.set('trust proxy', 1) // trust first proxy } this.#app.use(session(sess)); this.#app.use(async (req, res, next)=> { // Website you wish to allow to connect res.setHeader('Access-Control-Allow-Origin', req.headers.origin ?? ""); // Request methods you wish to allow res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE'); // Request headers you wish to allow res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type, Authorization'); // Set to true if you need the website to include cookies in the requests sent // to the API (e.g. in case you use sessions) res.setHeader('Access-Control-Allow-Credentials', 'true'); const session = req.session as Session; console.log(req.session.id, req.method, req.originalUrl); if (!session.user) { const access = await TokenCollection.CreateAccessToken(); session.user = access.token; req.session.save((err) => { if (err) { next(Utils.GetError('Error creating session', 500)); } else { next(); } }); } else { console.log("FOUND USER"); next(); } });
客户端代码
response = await fetch("http://localhost:3002/api/user", { method: "POST", body: JSON.stringify(profile_form_data), headers: { 'authorization': `Bearer ${access.token}`, 'content-type': 'application/json' }, credentials: 'include' });
注:当前处于开发模式,
process.env.NODE_ENV === 'production'为false
问题分析与修复方案
1. CORS配置重复冲突
已经通过cors中间件配置了跨域规则,后续又手动设置Access-Control-*响应头,导致规则不一致——尤其是Access-Control-Allow-Origin被动态设置为请求来源,和cors中间件中指定的WEB_CLIENT_URL可能冲突,浏览器会因规则不匹配拒绝携带Cookie。
修复:移除手动设置的所有Access-Control-*头,完全依赖cors中间件处理跨域,仅保留会话逻辑:
// 删除手动设置跨域头的代码块,保留会话处理逻辑 this.#app.use(async (req, res, next)=> { const session = req.session as Session; console.log(req.session.id, req.method, req.originalUrl); if (!session.user) { const access = await TokenCollection.CreateAccessToken(); session.user = access.token; req.session.save((err) => { if (err) { next(Utils.GetError('Error creating session', 500)); } else { next(); } }); } else { console.log("FOUND USER"); next(); } });
2. Cookie配置错误
开发模式下使用http协议,当前Cookie配置存在两个关键问题:
sameSite: "none":浏览器要求sameSite=none必须配合secure: true(仅HTTPS环境有效),开发模式下secure为false,浏览器会直接拒绝存储该Cookie。httpOnly: false:开发模式下也应该开启httpOnly,这是安全最佳实践,且不影响浏览器自动携带Cookie(前端本就不应读取会话Cookie)。
修复:调整Cookie配置,适配开发环境:
var sess = { secret: 'keyboard cat', saveUninitialized: true, // 修改为true,确保会话能被初始保存 resave: false, cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, // 始终开启HttpOnly sameSite: process.env.NODE_ENV === 'production' ? "none" : "lax", // 开发用lax,生产用none(配合secure) maxAge: 24 * 60 * 60 * 1000, domain: process.env.NODE_ENV === 'production' ? '.yourdomain.com' : undefined // 开发环境无需设置domain }, }
3. saveUninitialized配置问题
原配置saveUninitialized: false意味着只有当会话被修改后才会保存,但第一次创建会话时,即使调用了req.session.save(),如果Cookie未被客户端存储,下次请求仍会生成新会话。设置为true可以确保初始会话被创建并发送Cookie。
4. 验证WEB_CLIENT_URL值
确保WEB_CLIENT_URL被正确设置为'http://localhost:5173',必须和客户端运行的地址完全一致,包括协议(http)和端口。
内容的提问来源于stack exchange,提问作者omega

