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

前后端分离项目中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}`);
  });
});

问题根源

  1. 前端未携带跨域凭证:fetch请求未配置credentials: 'include',跨域时浏览器不会发送cookie,后端无法识别会话。
  2. CORS配置位置错误:在login中间件中设置CORS头,无法覆盖OPTIONS预检请求,导致浏览器拦截后续请求的凭证发送。
  3. 异步会话操作未正确等待:req.session.regenerate是异步操作,当前代码在回调未执行完成就发送响应,导致会话数据未持久化,返回的sessionID可能无效。
  4. Cookie参数配置不合理:sameSite: false在现代浏览器中需要配合secure: true才能生效,否则浏览器可能拒绝发送cookie;httpOnly: false增加XSS风险且不必要。
  5. 会话中间件挂载顺序可能有误:若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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 04:17:34