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

前后端跨子域部署下Session不持久、Cookie无法留存问题求助

跨子域Session与Cookie留存问题排查与解决

问题描述

开发前后端分属不同子域的Web应用时,遭遇Session与Cookie相关异常,需确保Session正确保存至MySQL存储、Cookie跨请求正常留存。

环境配置

  • 前端:React应用,运行地址 http://local.example.com:5173
  • 后端:Express.js服务,基于Passport.js实现身份认证,运行地址 http://api.local.example.com:3003
  • 会话管理:使用express-session搭配MySQL session store

问题现象

  • 用户登录后,后端未将Session数据写入MySQL会话存储
  • 登录成功后浏览器网络面板可见Set-Cookie头,但页面刷新后Cookie消失
  • 再次登录时系统会创建全新的Session

补充细节

  1. 已配置CORS允许前端域名请求,且设置credentials: true以确保请求携带Cookie
  2. 请求受保护路由时,req.user能正常获取,说明Passport.js认证逻辑正常
  3. 登录响应中存在Set-Cookie头,但Cookie无法在后续请求中自动携带
  4. 移除CORS中间件后,Cookie能保存到会话存储,但前端后续请求不再携带Cookie

相关代码

app.js

const express = require("express");
const passport = require("passport");
const app = express();
const auth = require("./routes/auth");
const order = require("./routes/order");
const session = require("express-session");
const MySQLStore = require("express-mysql-session")(session);
const env = require("dotenv").config();
const cors = require("cors");

app.use(
  cors({
    origin: "http://app.local.example.com:5174",
    credentials: true,
    methods: ["GET", "POST", "PUT", "DELETE"],
  })
);

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use((req, res, next) => {
  console.log("Session ID from req:", req.sessionID);
  console.log("Session data:", req.user);
  next();
});

const options = {
  host: process.env.HOST,
  user: process.env.USER,
  password: process.env.PASSWORD,
  database: process.env.NAME,
};

const sessionStore = new MySQLStore(options);

sessionStore
  .onReady()
  .then(() => {
    console.log("MySQLStore ready");
  })
  .catch((error) => {
    console.error(error);
  });

app.use(
  session({
    secret: process.env.SECRET,
    store: sessionStore,
    resave: false,
    saveUninitialized: true,
    cookie: {
      secure: false,
      maxAge: 1000 * 60 * 60 * 24,
      domain: ".local.example.com",
    },
  })
);

require("./config/passportConfig");
app.use(passport.initialize());
app.use(passport.session());

app.use("/api/account", auth);
app.use("/api/order", order);

app.listen(3003, () => {
  console.log("Listening on port 3003...");
});

passport config

const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;
const { getUserByEmail } = require("../models/user");
const { verifyPassword } = require("../services/passwordUtils");
const { getUserById } = require("../models/user");

async function verifyUser(email, password, done) {
  try {
    const user = await getUserByEmail(email);

    if (user) {
      const isPasswordVerified = await verifyPassword(password, user.pass);
      if (isPasswordVerified) {
        return done(null, user);
      }
      return done(null, false, { message: "Password is incorrect" });
    }
    return done(null, false, { message: `User ${email} does not exist` });
  } catch (err) {
    return done(err);
  }
}

const strategy = new LocalStrategy(
  {
    usernameField: "email",
    passwordField: "pass",
  },
  verifyUser
);

passport.use(strategy);

passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser(async (userId, done) => {
  try {
    const user = await getUserById(userId);
    return done(null, user);
  } catch (err) {
    done(err);
  }
});

Login.jsx

export default function LoginForm() {
  const [formData, setFormData] = useState({
    email: "",
    pass: "",
  });

  const [error, setError] = useState(false);

  function handleChange(event) {
    const { value, name } = event.target;

    setFormData((prev) => {
      return {
        ...prev,
        [name]: value,
      };
    });
  }

  async function validateLogin() {
    try {
      const data = await fetch(
        "http://api.local.example.com:3003/api/account/login",
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify(formData),
          credentials: "include",
        }
      );
      console.log("hi");
      return data;
    } catch (err) {
      return err;
    }
  }
}

解决方案

1. 修正CORS配置的origin值

你的前端实际运行地址是http://local.example.com:5173,但当前CORS配置的origin为http://app.local.example.com:5174,地址不匹配会导致浏览器拒绝保存Cookie。修改CORS配置:

app.use(
  cors({
    origin: "http://local.example.com:5173",
    credentials: true,
    methods: ["GET", "POST", "PUT", "DELETE"],
  })
);

2. 配置Cookie的SameSite属性

跨子域场景下,需明确设置Cookie的SameSite属性,避免浏览器限制Cookie发送。在express-session的cookie配置中添加:

cookie: {
  secure: false, // 本地开发用false,生产HTTPS环境需设为true
  maxAge: 1000 * 60 * 60 * 24,
  domain: ".local.example.com",
  sameSite: "Lax", // 跨域场景推荐用"Lax",若需跨站第三方请求可设为"None"(需配合secure: true)
}

3. 手动触发Session保存

Passport.js登录后修改req.session,但部分场景下需手动触发保存操作。在登录路由处理逻辑中添加:

// 示例登录路由处理
router.post('/login', passport.authenticate('local'), (req, res) => {
  req.session.save(err => {
    if (err) return res.status(500).send('Session保存失败');
    res.send('登录成功');
  });
});

4. 检查MySQL权限与会话表

确保MySQL用户拥有会话表(默认表名sessions)的读写权限,可手动查询表数据确认Session是否正常写入。

5. 统一前端请求的credentials配置

封装全局fetch工具函数,确保所有需携带Cookie的请求都设置credentials: 'include':

async function apiFetch(url, options = {}) {
  return fetch(url, {
    ...options,
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      ...options.headers,
    },
  });
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 18:32:12