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

Express-Session多用户会话异常:后续用户无法生成独立会话求助

多会话管理问题:后续用户登录无法创建独立会话

我正在开发用户认证功能,将JWT令牌存储在MongoDB的session集合中,但遇到多会话管理问题:第一个用户登录成功后,后续用户无法生成并存储独立会话。

测试步骤:

  • 使用Thunder Client和MongoDB Compass测试
  • 第一个用户登录凭据:
{
  "email": "mile@email.com",
  "password": "asdfAsdf1!"
}

会话成功创建并存入session集合,生成connect.sid cookie。

  • 第二个用户登录凭据:
{
  "email": "john@email.com",
  "password": "asdfAsdf2!"
}

此时第一个用户的会话ID被重新生成,而非创建全新独立会话,之前的会话也不会保留,connect.sid cookie保持不变,始终只有一个会话对象。


相关代码

会话配置

app.use(
  session({
    secret: "key_that_will_sign_the_cookie",
    resave: false,
    saveUninitialized: false,
    store: MongoStore.create({
      mongoUrl: "mongodb://127.0.0.1:27017/addressBook",
      collectionName: "sessions",
    }),
    cookie: {
      secure: false,
      maxAge: 15 * 60 * 1000,
    },
  })
); 

userController.ts

export const logIn = asyncHandler(async (req: Request, res: Response) => {
  const { email, password } = req.body;
  const user = await userService.login(email, password);
  const token = tokenService.generateToken(
    user._id,
    "ACCESS_TOKEN_PRIVATE_KEY_HERE"
  );

  // Set session
  (req.session as MySession).jwtToken = token;

  res.status(200).json({
    success: true,
    message: "You have successfully logged in.",
  });
});


export const logout = asyncHandler(async (req: Request, res: Response) => {
  req.session.destroy((err) => {
    if (err) {
      console.log("Error destroying session", err.message);
      res.status(500).json({
        success: false,
        message: "Error logging out.",
      });
    } else {
      res.clearCookie("connect.sid");
      res
        .status(200)
        .json({ success: true, message: "Successfully logged out." });
    }
  });
});

requireAuth.ts(中间件)

const requireAuth = async (req: Request, res: Response, next: NextFunction) => {
  try {
    const { jwtToken } = (req.session as MySession) || {};

    if (!jwtToken) {
      return res.status(401).json({ messagE: messages.unauthorized });
    }

    const { sub: userId } = jwt.verify(
      jwtToken,
      "ACCESS_TOKEN_PRIVATE_KEY_HERE"
    );

    const user = await User.findOne({ _id: userId });

    if (!user) {
      return res.status(401).json({ messagE: messages.unauthorized });
    }

    req.user = user;
    next();
  } catch (error: any) {
    return res.status(401).json({ message: error.message });
  }
};

更新说明

使用session是为了代码分层,将业务逻辑拆分到不同文件。我使用jsonwebtoken生成认证令牌并存储在session中,后续通过requireAuth中间件验证JWT有效性,用于保护所有联系人路由:

router.use(requireAuth);

router
  .get("/", getAllContacts)
  .get("/favorite", getFavoriteContacts)
  .get("/:contactId", getContactById)
  .post("/", addNewContact)
  .patch("/:contactId", updateContact)
  .delete("/:contactId", deleteContact);

tokenService.ts

import jwt from "jsonwebtoken";
import mongoose from "mongoose";

export const generateToken = (
  userId: mongoose.Types.ObjectId,
  secret: string
): string => {
  const payload = {
    sub: userId,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 15 * 60,
  };

  return jwt.sign(payload, secret);
};

export default { generateToken };

userService.ts

const login = async (email: string, password: string): Promise<IUserDoc> => {
  loginValidation(email, password);

  const user = await getUserByEmail(email);
  const isValidUser = user && (await user.isPasswordMatch(password));

  if (!isValidUser) {
    throw new ApiError(400, messages.incorrectCredentials);
  }

  return user;
};

问题原因与解决方案

问题根源

核心问题有两个:

  1. 测试场景限制:使用同一个客户端(Thunder Client)发起请求时,客户端会自动复用已有的connect.sid cookie,导致第二个用户的登录请求携带第一个用户的会话标识,后端直接更新该会话内容而非创建新会话。
  2. 登录逻辑未处理多会话场景:express-session默认基于请求携带的cookie匹配已有会话,仅当请求无会话cookie时才会创建新会话,现有代码未在登录时强制生成新会话。

解决方案

1. 模拟多客户端测试

在Thunder Client中测试第二个用户登录时,需:

  • 打开新的请求标签页,或在当前标签页的「Cookie」选项卡删除connect.sid
  • 或使用不同浏览器/隐私窗口发起第二个用户的登录请求
    以此模拟不同用户的独立会话环境,验证多会话创建逻辑。

2. 登录时强制生成新会话

若需在用户登录时,无论当前是否存在会话,都为新用户创建独立会话,可修改userController.ts的logIn函数:

export const logIn = asyncHandler(async (req: Request, res: Response) => {
  const { email, password } = req.body;
  const user = await userService.login(email, password);
  const token = tokenService.generateToken(
    user._id,
    "ACCESS_TOKEN_PRIVATE_KEY_HERE"
  );

  // 先销毁已有会话(如果存在)
  await new Promise<void>((resolve, reject) => {
    req.session.destroy((err) => {
      if (err) reject(err);
      else resolve();
    });
  });

  // 重新生成新会话
  req.session.regenerate((err) => {
    if (err) {
      return res.status(500).json({
        success: false,
        message: "Failed to create session."
      });
    }

    // 存储新JWT令牌到新会话
    (req.session as MySession).jwtToken = token;

    res.status(200).json({
      success: true,
      message: "You have successfully logged in.",
    });
  });
});

3. 优化会话配置(增强安全性与隔离性)

调整会话配置,提升多会话管理的安全性:

app.use(
  session({
    secret: "key_that_will_sign_the_cookie",
    resave: false,
    saveUninitialized: false,
    name: "user_session", // 自定义cookie名称,避免默认值冲突
    store: MongoStore.create({
      mongoUrl: "mongodb://127.0.0.1:27017/addressBook",
      collectionName: "sessions",
    }),
    cookie: {
      secure: process.env.NODE_ENV === "production", // 生产环境启用HTTPS传输
      maxAge: 15 * 60 * 1000,
      httpOnly: true, // 禁止前端JS访问cookie,防范XSS
      sameSite: "strict", // 限制跨站cookie传输,防范CSRF
    },
  })
); 

验证方法

完成修改后:

  1. 第一个用户登录,MongoDB的sessions集合新增一条会话记录,客户端获取到对应cookie
  2. 清除客户端cookie或用新窗口登录第二个用户,MongoDB新增第二条独立会话记录
  3. 分别用两个用户的cookie请求受保护路由,验证权限隔离是否正常

内容的提问来源于stack exchange,提问作者user5618326

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 03:12:05