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

MERN部署后express-session Cookie未保存/传递至前端问题

MERN电商网站跨域登录会话失效问题排查与修复

问题概述

基于MERN栈开发的电商网站,本地测试时通过express-session结合Cookie跟踪登录用户完全正常,但前端部署到Vercel、后端部署到Render后,登录状态验证请求返回403错误,Google/Facebook OAuth登录也存在相同问题。已尝试同域名部署、调整Cookie的domain和sameSite参数,但问题未解决。


相关代码

登录接口代码

/**
   * @desc 邮箱密码验证登录
   * @route POST /auth/login
   * @access 公开接口
   * @param req
   * @param res
   * @returns
   */
  static async login(req: Request, res: Response) {
    const { email, password } = req.body;

    if (!email || !password) {
      return res.status(400).json({ status: 'error', message: '所有字段为必填项!' });
    }

    // 检查用户是否存在
    const checkUser = await User.findOne({ email });
    if (!checkUser) {
      return res.status(401).json({ message: '未授权!', status: 'error' });
    }

    // 验证密码是否匹配
    const matchPassword = await bcrypt.compare(password, checkUser.password);

    if (!matchPassword) {
      return res.status(401).json({ message: '未授权', status: 'error' });
    }

    req.session.user = checkUser;
    req.session.userId = checkUser._id.toString();

    delete checkUser.password;
    console.log('用户会话ID: ', req.session.id);

    return res.json({
      status: 'success',
      data: checkUser,
      message: '登录成功',
    });
  }

后端入口文件初始配置

dotenv.config();

const PORT = process.env.PORT || 3500;
const secretKey = process.env.EXPRESS_SESSION_KEY as string;
const CLIENT_DOMAIN = process.env.CLIENT_DOMAIN as string;
const app = express();
const isProduction = process.env.NODE_ENV === 'production';

// ...其他导入

const __dirname = url.fileURLToPath(new URL('.', import.meta.url));

AdminJS.registerAdapter({ Database, Resource });

const start = async () => {
  const mongoDB = await initializeDb();

  const admin = new AdminJS({
    ...options,
    databases: [mongoDB.db],
  });

  if (process.env.NODE_ENV === 'production') {
    await admin.initialize();
  } else {
    admin.watch();
  }

  const router = buildAuthenticatedRouter(
    admin,
    {
      cookiePassword: process.env.COOKIE_SECRET,
      cookieName: 'adminjs',
      provider,
    },
    null,
    {
      secret: process.env.COOKIE_SECRET,
      saveUninitialized: true,
      resave: true,
    }
  );

  app.use(admin.options.rootPath, router);
  configurePassport();

  app.use(logger);
  app.use(cookieParser());

  // cors
  app.use(cors(corsOptions));

  // 会话认证配置
  const sessionMiddleware = expresssession({
    secret: secretKey,
    resave: true,
    saveUninitialized: true,
    cookie: {
      maxAge: 1000 * 60 * 60 * 10,
      // TODO : 修改配置
      secure: !!isProduction,
      sameSite: isProduction ? 'none' : 'lax',
      domain: !isProduction ? 'localhost' : '.orender.com ',
      httpOnly: true,
    },
    name: 'oubuntu_cookie',
  });
  app.use(express.json());
  app.use(express.urlencoded({ extended: true }));
  app.use(sessionMiddleware);
  app.use(passport.initialize());
  app.use(passport.session());
  // 静态文件
  app.use((req, res, next) => {
    res.header('Access-Control-Allow-Credentials', 'true');
    res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS');

    if (req.method === 'OPTIONS') {
      res.sendStatus(200);
    } else {
      next();
    }
  });
  app.use('/', express.static(join(process.cwd(), 'src', 'public')));

  // ...路由配置

  app.listen(PORT, () => {
    console.log('服务器运行地址: ', `http://localhost:${PORT}`);
    console.log(`AdminJS启动地址 http://localhost:${PORT}${admin.options.rootPath}`);
  });
};

start();

认证路由配置

const authRoute = express.Router();
authRoute
  .post('/login', AuthenticationController.login)
  .get('/logout', AuthenticationController.logout)
  .get('/status', AuthenticationController.getUserStatus);

调整后的express-session配置

// cors
  app.use(cors(corsOptions));

  // 会话认证配置
  const sessionMiddleware = expresssession({
    secret: secretKey,
    resave: true,
    saveUninitialized: true,
    cookie: {
      maxAge: 1000 * 60 * 60 * 10,
      // TODO : 修改配置
      secure: isProduction,
      sameSite: isProduction ? 'none' : 'lax',
      domain: !isProduction ? 'localhost' : 'oubuntu-frontend-ori.onrender.com',
      httpOnly: true,
    },
    name: 'oubuntu_cookie',
  });
  app.use(express.json());
  app.use(express.urlencoded({ extended: true }));
  app.use(sessionMiddleware);
  app.use(passport.initialize());
  app.use(passport.session());
  // 静态文件
  app.use((req, res, next) => {
    res.header('Access-Control-Allow-Credentials', 'true');
    res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS');

    if (req.method === 'OPTIONS') {
      res.sendStatus(200);
    } else {
      next();
    }
  });
  app.use('/', express.static(join(process.cwd(), 'src', 'public')));

问题排查与修复方案

  • 初始配置中domain: '.orender.com '末尾有空格,会导致Cookie无法正确绑定域名
  • 调整后的配置将domain设为前端域名是错误的:Cookie的domain应设置为后端域名,或留空让浏览器自动处理(推荐)。跨域场景下,domain不能设为前端域名,否则浏览器不会携带Cookie到后端。

修复:
生产环境下移除domain配置:

cookie: {
  maxAge: 1000 * 60 * 60 * 10,
  secure: isProduction,
  sameSite: isProduction ? 'none' : 'lax',
  httpOnly: true,
  // 移除domain配置,由浏览器自动绑定当前请求域名
}

2. CORS配置缺失关键参数

跨域携带Cookie必须满足:

  • 后端CORS配置的origin设为前端具体域名(不能是*)
  • 开启credentials: true

修复:
明确配置CORS选项:

const corsOptions = {
  origin: process.env.CLIENT_DOMAIN, // 示例:'https://oubuntu-frontend-ori.vercel.app'
  credentials: true,
  optionsSuccessStatus: 200
};
app.use(cors(corsOptions));

3. 中间件顺序错误

当前代码中请求体解析中间件放在了会话中间件之后,会导致请求体解析滞后,影响会话初始化。正确顺序应为:

  1. 请求体解析(express.json、express.urlencoded)
  2. Cookie解析(cookieParser)
  3. CORS配置
  4. Session中间件
  5. Passport初始化

修复:
调整中间件顺序:

// 先解析请求体
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Cookie解析
app.use(cookieParser());

// CORS配置
app.use(cors(corsOptions));

// 会话中间件
app.use(sessionMiddleware);

// Passport初始化
app.use(passport.initialize());
app.use(passport.session());

4. 重复设置CORS响应头

当前代码中自定义中间件重复设置CORS相关头,会与cors中间件的配置冲突,导致浏览器无法正确识别。

修复:
删除该自定义中间件,所有CORS配置统一由cors(corsOptions)处理。

5. OAuth登录额外注意事项

对于Google/Facebook OAuth登录,需确保:

  • OAuth平台配置的回调地址为后端真实域名(示例:https://your-backend.onrender.com/auth/google/callback)
  • Passport OAuth策略配置中使用绝对路径作为回调地址
  • 后端会话配置覆盖OAuth路由

验证步骤

  1. 部署修复后的后端代码到Render
  2. 前端请求时确保携带withCredentials: true(Axios示例:axios.get('/auth/status', { withCredentials: true }))
  3. 打开浏览器开发者工具,查看Application标签下的Cookie是否正确设置(域名、secure、sameSite属性符合预期)
  4. 检查请求头中是否携带Cookie字段

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 00:49:53