子域名跨域Cookie隔离:客户端与管理端会话独立方案咨询
我们的后端部署在https://api.example.com,另有两个客户端应用分别托管于https://client.example.com和https://admin.example.com,二者均调用同一后端接口。
我们计划通过HttpOnly Cookie实现会话管理,但需要确保client与admin的会话互不干扰——即同一浏览器的不同标签页可分别登录两个应用的不同账号。此外,我们还有结构相同的测试环境:后端位于https://api-test.example.com,应用分别在https://client-test.example.com和https://admin-test.example.com,需保证测试环境与生产环境的会话完全隔离。
当前后端基于Express实现的核心逻辑如下:
express .use((req, res, next) => { if (req.headers.origin && allowedOrigins.includes(req.headers.origin)) { res.setHeader("access-control-allow-credentials", "true") res.setHeader("access-control-allow-origin", req.headers.origin) res.setHeader("access-control-allow-methods", "GET,POST,PUT,DELETE,OPTIONS,HEAD") res.setHeader("access-control-allow-headers", ...) } next(); }) .post("/login", (req, res) => { //校验登录信息 //... res.setHeader("set-cookie", `sessionid=${sessionid}; HttpOnly; SameSite=Strict;`); res.sendStatus(204); })
客户端通过fetch发起请求的示例如下:
fetch("https://api.example.com/ajax", {method: "POST", credentials: "include", ... })
目前基础功能正常:登录时后端会设置Domain为api.example.com、SameSite为Strict的Cookie,后续请求携带credentials: "include"时会自动带上该Cookie。
但当前存在问题:打开admin应用的标签页时,会与client应用共享同一个Cookie,导致admin请求后端时携带client的会话ID,且任一应用登录都会覆盖对方的会话。
我们尝试将Cookie的Domain属性设为请求的Origin,但浏览器开发者工具报错:
This Set-Cookie was blocked because its Domain attribute was invalid with regards to the current host url.
仅当Domain设为api.example.com或example.com时Cookie才能正常设置,但前者仍会导致client与admin共享会话,后者会使生产环境(api.example.com)与测试环境(api-test.example.com)的Cookie互相共享。
作为备选方案,我们考虑根据请求Origin命名Cookie,并在后端根据Origin校验对应的Cookie。但想咨询是否有其他Cookie属性或HTTP Header配置方式可实现需求?是否存在我们忽略的方案,或该需求本身无法实现?欢迎提供建议。
1. 根据请求Origin区分Cookie名称
这是最直接且兼容所有浏览器的方案,完全匹配你的需求:
- 登录时,后端解析请求的
Origin头,根据来源生成对应名称的Cookie,比如给client.example.com生成sessionid_client,给admin.example.com生成sessionid_admin - Cookie的Domain仍设为
api.example.com,确保客户端请求API时能携带对应Cookie - 后端处理请求时,根据
Origin头选择对应的Cookie进行会话校验 - 测试环境的Cookie会自动与生产环境隔离(因为Domain为
api-test.example.com,和生产环境的api.example.com完全独立)
修改后的登录接口示例:
.post("/login", (req, res) => { //校验登录信息 //... const origin = req.headers.origin; let cookieName = "sessionid"; if (origin.includes("client.example.com")) { cookieName = "sessionid_client"; } else if (origin.includes("admin.example.com")) { cookieName = "sessionid_admin"; } // 测试环境可通过origin中的-test标识区分 if (origin.includes("-test.example.com")) { cookieName += "_test"; } res.setHeader("set-cookie", `${cookieName}=${sessionid}; HttpOnly; SameSite=Strict; Domain=api.example.com;`); res.sendStatus(204); })
2. 使用Cookie的Partitioned属性(现代浏览器支持)
这是Chromium系浏览器(Chrome、Edge等)支持的新属性,用于实现跨站点Cookie分区存储:
- 设置Cookie时添加
Partitioned属性,同时必须指定SameSite=None和Secure(该属性仅针对跨站点场景生效) - 浏览器会为
client.example.com和admin.example.com分别存储独立的sessionidCookie,即使它们请求同一个API域名 - 测试环境因Domain独立,无需额外处理
注意:该属性目前仅在Chromium系浏览器支持,Firefox和Safari尚未兼容,无法作为通用方案。
示例Cookie设置:
res.setHeader("set-cookie", `sessionid=${sessionid}; HttpOnly; SameSite=None; Secure; Partitioned; Domain=api.example.com;`);
3. 为客户端分配独立API子路径(可选)
如果允许调整API路由结构,可以通过路径隔离会话:
- 给client分配
https://api.example.com/client/*路径,admin分配https://api.example.com/admin/*路径 - 设置Cookie时指定对应的
Path属性:client的Cookie Path设为/client/,admin的设为/admin/ - 客户端请求对应路径时才会携带对应Cookie,实现会话隔离
该方案需要调整API路由和客户端请求路径,改动较大,适合有重构计划的场景。
最稳妥且兼容性最好的方案是根据Origin区分Cookie名称,可以完全满足会话隔离需求,且无需依赖浏览器新特性。Partitioned属性可作为未来的优化方向,但目前无法覆盖全量用户。
内容的提问来源于stack exchange,提问作者derpirscher

