生产环境PassportJS+Express+React授权Cookie异常求助
后端基于PassportJS+Express,前端React+Axios搭建的应用,本地运行时授权功能完全正常:客户端发送携带邮箱密码的POST请求,服务器返回Set-Cookie响应头,浏览器可正常存储Cookie。但部署到Vercel后出现以下问题:
- 客户端请求后,响应头存在Set-Cookie,但浏览器未存储该Cookie
- 直接通过服务器URL访问安全接口可正常返回数据,用Axios发起相同请求时,请求头携带的Cookie与直接访问时一致,却返回401未通过权限校验,偶尔又能正常通过校验获取数据
已配置的生产环境代码
CORS与Session配置
app.use( cors({ origin: process.env.ADMIN_PANEL_URL, methods: 'GET,POST,PUT,DELETE', credentials: true, exposedHeaders: ['set-cookie'], }) ); app.set('trust proxy', 1); app.use( session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: true, cookie: { sameSite: 'none', secure: true, maxAge: 1000 * 60 * 60 * 24, }, }) );
客户端Axios配置与请求代码
const api = axios.create({ baseURL: `${process.env.REACT_APP_SERVER_URL}`, withCredentials: true, headers: { Accept: "application/json", "Content-Type": "application/json", "Access-Control-Expose-Headers": "Set-Cookie", }, }); static async getStatistics() { return ( await api.get<IStatistic>(`/statistics`, { withCredentials: true, }) ).data; } static localLogin( email: string, password: string, setState: Dispatch<SetStateAction<boolean>>, setAlert: Dispatch< SetStateAction<{ isOpen: boolean; message: string; severity: AlertColor; }> > ) { axios({ method: "POST", data: { email: email, password: password, }, withCredentials: true, url: `${process.env.REACT_APP_SERVER_URL}/local`, }) .then((res) => { if (res.status === 200) { if (res.data.isAdmin) { localStorage.setItem(`isAuth`, JSON.stringify(true)); window.location.href = `${process.env.REACT_APP_CLIENT_URL}`; } else { setState(true); } } }) .catch((e) => { console.log(e); if (e.code !== "ERR_NETWORK" && !e.response.data.success) { setState(true); } else { setAlert({ isOpen: true, message: "Server error, try again later", severity: "error", }); } console.log(e); }); }
Passport LocalStrategy配置
router.post( '/local', passport.authenticate('local', { failureRedirect: '/login/fail', }), (req, res) => { res.json(req.user); } ); passport.use( 'local', new PassportLocal.Strategy( { usernameField: 'email' }, async (email, password, done) => { try { const user = await User.findOne({ email: email.toLowerCase() }); if (!user || !(await bcrypt.compare(password, user.password))) { done(null, false); } done(null, user); } catch (e) { done(e); } } ) ); passport.serializeUser((user, done) => { return done(null, user._id.toString()); }); passport.deserializeUser((id, done) => { User.findById(id, (err, user) => { return done(err, user); }); });
权限校验中间件
export const isAdminLoggedIn = (req, res, next) => { req.isAuthenticated() && req.user.isAdmin ? next() : res.sendStatus(401); };
针对Vercel部署后的Cookie异常问题,按以下步骤排查修复:
1. 给Session Cookie添加domain配置
Vercel部署的前后端通常属于不同子域名,浏览器默认不会跨域存储Cookie,必须显式指定主域名:
修改Session配置中的cookie字段:
cookie: { sameSite: 'none', secure: true, maxAge: 1000 * 60 * 60 * 24, domain: '.your-domain.com', // 替换为你的主域名,如前端是admin.your-domain.com、后端是api.your-domain.com时,设为该值 httpOnly: true // 建议添加,提升Cookie安全性,防止XSS攻击 }
注意:domain必须以.开头,确保所有子域名都能共享该Cookie;若前后端为同域名不同路径可省略,但Vercel部署场景下一般需要配置。
2. 确保CORS的origin精确匹配
Vercel部署后,前端实际URL可能与配置的ADMIN_PANEL_URL不一致(如带/不带www、使用Vercel临时域名),浏览器会严格校验origin,不匹配则拒绝存储Cookie:
- 检查环境变量
ADMIN_PANEL_URL的值,必须与前端部署后的实际URL完全一致(包含https://,结尾不要加/) - 测试阶段可临时将origin设为
true(生产环境禁止使用),验证是否为origin匹配问题:
cors({ origin: true, // 临时测试用,生产必须设为精确域名 credentials: true, // 其他配置保持不变 })
3. 统一使用Axios实例,避免配置不一致
你的localLogin方法使用了全局axios,而非预先创建的api实例,虽然都配置了withCredentials: true,但统一实例可避免潜在的配置差异:
修改localLogin方法:
static localLogin( email: string, password: string, setState: Dispatch<SetStateAction<boolean>>, setAlert: Dispatch< SetStateAction<{ isOpen: boolean; message: string; severity: AlertColor; }> > ) { api.post('/local', { email, password }) .then((res) => { // 原有逻辑保持不变 }) .catch((e) => { // 原有逻辑保持不变 }); }
另外,api实例已配置withCredentials: true,请求时无需重复添加,避免冗余。
4. 将Session从内存改为持久化存储(关键修复)
Vercel的Serverless函数是无状态的,每次请求可能分配到不同的函数实例,内存Session无法跨实例共享,这是偶尔出现401的核心原因。需改用Redis等持久化存储,推荐使用Upstash(Vercel官方推荐的Redis服务):
- 安装依赖:
npm install connect-redis redis - 修改Session配置:
const RedisStore = require('connect-redis').default; const redis = require('redis'); // 从Upstash获取Redis连接URL,存入环境变量 const client = redis.createClient({ url: process.env.REDIS_URL }); client.connect().catch(console.error); app.use( session({ store: new RedisStore({ client }), // 使用Redis存储Session secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, // 改为false,避免创建空Session cookie: { sameSite: 'none', secure: true, maxAge: 1000 * 60 * 60 * 24, domain: '.your-domain.com', httpOnly: true }, }) );
5. 移除Passport的重定向逻辑,直接返回JSON响应
passport.authenticate中的failureRedirect会在认证失败时触发重定向,可能导致Set-Cookie头被浏览器忽略,改为直接返回JSON响应:
router.post( '/local', (req, res, next) => { passport.authenticate('local', (err, user, info) => { if (err) return next(err); if (!user) return res.status(401).json({ success: false, message: '账号或密码错误' }); req.logIn(user, (err) => { if (err) return next(err); return res.json(user); }); })(req, res, next); } );
这样认证失败时不会触发跳转,浏览器可正确处理响应头中的Set-Cookie。
6. 检查浏览器第三方Cookie设置
Chrome等现代浏览器默认阻止第三方Cookie,若前后端为不同域名,需确保浏览器允许你的域名的第三方Cookie,或确认SameSite=None与secure: true已正确配置(你已配置,可再次核对)。
Chrome中可通过chrome://settings/cookies查看是否阻止了目标域名的Cookie。
最核心的两个问题:
- Vercel Serverless环境下内存Session无法跨实例共享,必须改用Redis持久化存储
- Cookie未配置
domain,导致跨子域名无法存储
按上述步骤逐一排查修复,即可解决部署后的Cookie授权问题。
内容的提问来源于stack exchange,提问作者UnionIM

