前后端分离项目中express-session会话数据获取失败问题排查
前后端分离下express-session身份认证问题排查与修复
场景概述
前后端分离项目,后端基于tsoa+express-session实现身份认证(端口4040),前端测试页面运行在端口5500。登录后浏览器已获取cookie,但后续请求无法完成身份认证:req.session中所有变量值为undefined,无法递增session_users。需基于express-session解决该问题。
现有代码
后端代码
Session.ts
// Session.ts class Session { public mount(_express: Application): Application { _express.set('trust proxy', 1) const sess: SessionOptions = { secret: Locals.config().appSecret, resave: false, saveUninitialized: true, // rolling: true, proxy:true, cookie: { maxAge: 3600 * 1000, // 1 hr httpOnly: false, secure: false, domain: undefined, sameSite: false, }, // cookie expire in half an hour from last activity store: MongoStore.create({ mongoUrl: Locals.config().mongooseUrl }) }; _express.use(session(sess)); return _express; } }
authController.ts
// authController.ts @Tags("Authentication") @Route("auth") export class AuthController extends Controller { @Post("/login") @Middlewares([AuthService.login]) public async login(@Body() body: loginInput): Promise<any> { return null; } }
authService.ts
// authService.ts export class AuthService { public async login(req: express.Request, res: express.Response, next: express.NextFunction) : Promise<void> { res.header('Access-Control-Allow-Credentials', "true"); res.header('Access-Control-Allow-Origin', req.headers.origin); res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); res.header('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept'); if (req.body.hasOwnProperty("username") && req.body.hasOwnProperty("password")) { const user = await User.findOne({login_name: req.body.username}); if (user) { if (bcrypt.compareSync(req.body.password, user.password)){ if (!req.session.isAuthenticated){ req.session.regenerate(function (err: any) { if (err) throw next(err); req.session.isAuthenticated = true; req.session.user_id = user._id; req.session.session_users = 1; req.session.save(function (err) {if (err) return next(err)}) }); res.status(200).send({user: user._id, sid: signature.sign(req.sessionID, Locals.config().appSecret)}); } else { req.session.session_users = (req.session.session_users) ? req.session.session_users++ : 1; res.status(200).send({user: user._id}); } } else { res.status(401).send({ reason: "Failed to login.", details: "Invalid password. Please try again." }); } } else { res.status(401).send( { reason: "Failed to login.",details: "Invalid username. Please try again." }); } } else { next() } } }
前端代码
测试HTML
<!DOCTYPE html> <html lang="en"> <head> </head> <button id="login-btn">Login button</button> <script src="script.js"></script> </html>
script.js
const login_button = document.getElementById('login-btn'); const data = { "username": "any valid username", "password": "any valid pw" } login_button.addEventListener('click', async _ => { fetch('http://localhost:4040/auth/login', { method: 'post', mode: 'cors', cache: 'default', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) }) .then(response => response.json()) .then(data => { console.log(data); window.localStorage.setItem('sid', data['sid']); }) .catch(error => { console.error(`Error: ${error}`); }); });
问题根源
- 前端未携带跨域凭证:fetch请求未配置
credentials: 'include',跨域时浏览器不会发送cookie,后端无法识别会话。 - CORS配置位置错误:在login中间件中设置CORS头,无法覆盖OPTIONS预检请求,导致浏览器拦截后续请求的凭证发送。
- 异步会话操作未正确等待:
req.session.regenerate是异步操作,当前代码在回调未执行完成就发送响应,导致会话数据未持久化,返回的sessionID可能无效。 - Cookie参数配置不合理:
sameSite: false在现代浏览器中需要配合secure: true才能生效,否则浏览器可能拒绝发送cookie;httpOnly: false增加XSS风险且不必要。 - 会话中间件挂载顺序可能有误:若session中间件在tsoa路由之后挂载,路由无法访问session对象。
修复方案
1. 前端修改:携带跨域凭证
在fetch请求中添加credentials: 'include',确保跨域时发送cookie:
login_button.addEventListener('click', async _ => { fetch('http://localhost:4040/auth/login', { method: 'post', mode: 'cors', cache: 'default', credentials: 'include', // 新增此行 headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) }) .then(response => response.json()) .then(data => { console.log(data); window.localStorage.setItem('sid', data['sid']); }) .catch(error => { console.error(`Error: ${error}`); }); });
2. 后端修改:统一配置CORS
移除login中间件中的CORS头设置,改用cors中间件统一配置,确保覆盖所有请求(包括OPTIONS预检):
// 先安装依赖:npm install cors @types/cors import cors from 'cors'; // 在Session.mount方法中,session中间件之后添加cors配置 public mount(_express: Application): Application { _express.set('trust proxy', 1) const sess: SessionOptions = { // ... 原有session配置 }; _express.use(session(sess)); // 添加CORS配置 _express.use(cors({ origin: req => req.headers.origin, // 生产环境建议指定具体域名,提升安全性 credentials: true, methods: ['GET', 'PUT', 'POST', 'DELETE'], allowedHeaders: ['X-Requested-With', 'X-HTTP-Method-Override', 'Content-Type', 'Accept'] })); return _express; }
3. 修复异步会话操作
将req.session.regenerate改为异步等待,确保会话数据持久化后再发送响应:
// authService.ts的login方法中修改 if (!req.session.isAuthenticated){ // 用Promise封装regenerate的异步操作 await new Promise<void>((resolve, reject) => { req.session.regenerate((err) => { if (err) return reject(err); req.session.isAuthenticated = true; req.session.user_id = user._id; req.session.session_users = 1; req.session.save((err) => { if (err) return reject(err); resolve(); }); }); }); // 等待会话保存完成后再响应 res.status(200).send({user: user._id, sid: signature.sign(req.sessionID, Locals.config().appSecret)}); } else { req.session.session_users = (req.session.session_users || 0) + 1; // 修正递增逻辑 await new Promise<void>((resolve, reject) => { req.session.save((err) => { if (err) reject(err); resolve(); }); }); res.status(200).send({user: user._id}); }
4. 优化Cookie配置
根据环境调整cookie参数,确保兼容性和安全性:
cookie: { maxAge: 3600 * 1000, httpOnly: true, // 开启httpOnly,防止XSS窃取cookie secure: process.env.NODE_ENV === 'production', // 生产环境开启secure(仅HTTPS) sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax', // 生产环境用none配合secure,本地用lax domain: process.env.NODE_ENV === 'production' ? '.yourdomain.com' : undefined, // 生产环境设置统一域名 }
5. 确认中间件挂载顺序
确保session中间件和cors中间件在tsoa路由中间件之前挂载,例如:
// 主入口文件 const app = express(); const sessionInstance = new Session(); sessionInstance.mount(app); // 先挂载session和cors // 再挂载tsoa的路由 import { RegisterRoutes } from './routes'; RegisterRoutes(app);
6. 验证MongoStore连接
检查MongoDB连接是否正常,确保session能正确存储。可在MongoStore中添加日志:
store: MongoStore.create({ mongoUrl: Locals.config().mongooseUrl, onConnected: () => { console.log('Session store connected to MongoDB'); }, onError: (err) => { console.error('Session store connection error:', err); } })
内容的提问来源于stack exchange,提问作者Thomas
相关产品推荐
相关产品推荐

