A 同事开了个VIP账号,B、C 同事也想用同个VIP。直接共享账号密码不安全,还有设备数量限制,把 Cookie 复制给每个人又麻烦还容易过期。有没有办法让 B、C 在各自浏览器里,自动复用 A 的登录态?
在 Web 内网穿透 / 全量代理场景下,这个功能叫会话共享。本文讲清楚怎么实现,以及为什么比看起来复杂。
一、场景:VIP共享会话问题
先看一个真实的场景:
- A 同事VIP账号登录了XX网站
- B、C 同事也想要访问这个网站,但没有VIP
传统方案的问题:
| 方案 | 问题 |
|---|---|
| 共享账号密码 | 安全风险大,权限失控,A 改密码后全员失效 |
| 复制 Cookie 给每人 | Cookie 过期快(几小时到几天),手动维护累,HttpOnly Cookie 还拿不到 |
| 给每人开账号 | 内网系统账号管理麻烦,权限审批慢,临时协作不值得。外部系统需要花钱买账号 |
期望效果:A 登录一次,B、C 通过一个共享链接直接获得 A 的登录态,零配置使用。会话过期时 A 重新登录,B、C 自动同步。
二、会话共享的核心设计
2.1 域名编码:会话身份写入子域
Web 全量代理通过子域名编码目标站点(详见系列第一篇)。会话共享在此基础上,把会话身份也编进子域名:
xml
普通代理:
www__example__com.<vpnDomain>/path
会话共享(A 是会话提供者):
www__example__com-main-<shareId>.<vpnDomain>/path
会话共享(B、C 是会话消费者):
www__example__com-share-<shareId>.<vpnDomain>/path
main:会话提供者,A 用这个域名访问,登录态会被采集share:会话消费者,B、C 用这个域名访问,自动复用 A 的登录态shareId:会话标识,A 生成一个分享链接,链接里带shareId,B、C 用同样的shareId
核心思路 :服务端根据子域名里的 -main- / -share-<id> 后缀识别会话身份,分别做"采集"和"注入"。
2.2 服务端:持久化会话存储
会话状态存在服务端的 sessionStore 里------一个落盘到 sessions/ 目录的键值存储(每个 key 一个 JSON 文件),键设计:
bash
<shareId>-cookie # A 的 Cookie
<shareId>-authorization # A 的 Authorization 头
<shareId>-clientCache # A 的 cookie + localStorage(客户端上报)
为什么用文件落盘而不是内存?
- 多进程/多实例天然共享:不用 IPC 广播,任何工作进程读写都是同一份文件
- 重启不丢:服务重启后共享链接依然有效
- 原子写:写入用「写临时文件 → rename」原子替换,读者要么看到旧完整文件、要么看到新完整文件,永远不会读到半截,多进程并发无需文件锁
- 带 TTL 自动清理:每个条目带过期时间(默认 30 分钟,A 每次访问都会续期),服务启动时统一清理过期文件
为什么 Cookie、Authorization、localStorage 要分开存?
- Cookie:服务端能直接读到(请求头),服务端层面采集
- localStorage:服务端读不到(只在浏览器),需要客户端 JS 上报
- Authorization 头:服务端能读到,单独存
分别处理,因为它们的生命周期和访问方式不同。另外 shareId 来自客户端、要拼进文件路径,必须 sanitize(只允许字母数字与 _ -,限长),杜绝路径穿越。
2.3 main 端:服务端采集 Cookie
A 用 -main-<shareId> 域名访问时,服务端从请求头里提取 Cookie 和 Authorization:
js
async function checkShareSession(ctx) {
// 从子域名后缀 -(main|share)-<shareId> 解析会话身份(正则兼容两种域名编码模式)
const { shareId, isMainSession } = parseShareSuffix(ctx.subdomain)
if (isMainSession) {
// A 的请求,采集登录态
const cookie = ctx.headers.cookie
const auth = ctx.headers.authorization
if (cookie) await sessionStore.setItem(`${shareId}-cookie`, cookie)
if (auth) await sessionStore.setItem(`${shareId}-authorization`, auth)
} else {
// B/C 的请求,注入登录态
const cookie = await sessionStore.getItem(`${shareId}-cookie`)
const auth = await sessionStore.getItem(`${shareId}-authorization`)
if (cookie) ctx.headers.cookie = cookie
if (auth) ctx.headers.authorization = auth
}
}
关键 :A 每次访问都会刷新 sessionStore 里的 Cookie。Cookie 过期前 A 重新登录,B、C 自动同步新的 Cookie------会话续期自动化。
2.4 main 端:客户端上报 localStorage
光 Cookie 不够。现代网站的登录态很多存在 localStorage 里(JWT、token 之类)。这部分服务端读不到,需要客户端 JS 上报。
A 的页面里注入一段脚本(share-sessions.js):
js
// 首次延迟 3s 上报(等登录态写入),之后进入定期上报循环
const tick = async () => {
await report() // POST { cookie: document.cookie, localStorage: {...localStorage} }
await sleep(document.hidden ? 30000 : 10000)
tick()
}
setTimeout(tick, 3000)
// 页面隐藏时立即上报一次
document.addEventListener('visibilitychange', () => {
if (document.hidden) report()
})
// 页面卸载时用 sendBeacon 兜底(beforeunload 里 fetch 可能被浏览器中止)
window.addEventListener('beforeunload', () => {
navigator.sendBeacon(url, new Blob([JSON.stringify(clientCache)], { type: 'application/json' }))
})
服务端收到后存入 sessionStore:
js
// serveWww 里处理 /share-sessions
if (ctx.url.startsWith('/share-sessions')) {
const body = await this.calcRequestBody(ctx)
await sessionStore.setItem(ctx.query.shareId + '-clientCache', body)
}
为什么首次延迟 3 秒? 网站登录后通常会有几个异步请求把 token 写入 localStorage,立即上报可能拿到空值。3 秒是经验值。
为什么要定期上报? 早期版本只上报一次,之后 Cookie/localStorage 变化(比如 token 刷新、子系统登录)就无法同步给 share 会话了。改为可见时每 10 秒上报一次、页面隐藏时降频、隐藏/卸载时立即上报------保证 A 的登录态变化自动同步给 B、C。
2.5 share 端:客户端回填 localStorage
B、C 用 -share-<shareId> 域名访问时,服务端在页面里注入恢复脚本:
js
// 注入到 share 会话页面(服务端渲染时直接内联 clientCache 数据)
const clientCache = JSON.parse(embeddedClientCache)
if (clientCache) {
// 回填 Cookie
document.cookie = clientCache.cookie
// 回填 localStorage
for (const [k, v] of Object.entries(clientCache.localStorage)) {
localStorage.setItem(k, v)
}
}
完整链路:
css
A 登录 → main 域名访问
↓
服务端采集 Cookie/Authorization → sessionStore
客户端 3s 后首次上报、此后定期上报 → sessionStore
↓
B/C 用 share 域名访问
↓
服务端注入 Cookie/Authorization 到请求头(透明转发到目标站)
客户端注入恢复脚本 → 回填 Cookie/localStorage
↓
B/C 看到已登录的页面
三、关键技术点
3.1 Cookie 的 domain 处理
Web 全量代理里,目标站设置的 Cookie 默认带 domain=target.com。但 B、C 用的是 -share-<id> 子域,Cookie 的 domain 不匹配会丢。
服务端需要改写 Set-Cookie 的 domain:
- original 模式 (多级子域名):
domain=www.example.com→domain=www.example.com-main-<id>.<vpnDomain> - underline 模式 (单段子域名):所有 Cookie domain 塌缩到
vpnDomain,所有子域共享------会话共享天然支持
会话共享场景下 underline 模式更简单,但牺牲了跨子域 Cookie 隔离。这是 trade-off。
3.2 share 端的 Set-Cookie 覆盖
B、C 用 share 域名访问时,目标站可能返回新的 Set-Cookie(比如刷新 token)。但 B、C 不应该刷新 A 的会话------他们只是消费者。
服务端处理:
js
if (!isMainSession && shareId) {
// share 端,把 sessionStore 里缓存的 A 的 Cookie 合并进响应的 Set-Cookie
const cachedCookie = await sessionStore.getItem(`${shareId}-cookie`)
if (cachedCookie) {
// 缓存的是 Cookie 请求头格式("a=1; b=2"),拆成单条后与目标响应自身的
// set-cookie 合并,避免覆盖目标站点新设置的 cookie
const cached = cookie.split(';').map(c => c.trim()).filter(Boolean)
headers['set-cookie'] = [...cached, ...existing]
}
}
B、C 的浏览器只存 A 的 Cookie,不存目标站新发的 Cookie。保证会话状态始终由 A 主导。
3.3 多进程与持久化
生产环境可能开 cluster 多进程,A 的请求可能落到进程 1,B、C 的请求可能落到进程 2、3。
如果会话存内存,就得做 IPC 同步。本项目直接把 sessionStore 落盘到 sessions/ 目录,多进程读写同一份文件,天然共享,无需 IPC;配合「写临时文件 → rename」原子替换 + 带 TTL 的读缓存(3 秒),既不丢数据也不高频读盘。服务重启会话也不丢。
3.4 安全考虑
会话共享有安全风险,必须考虑:
| 风险 | 缓解 |
|---|---|
| shareId 泄露,任何人复用 | shareId 用足够长的随机串(首页生成分享链接时自动生成),且服务端对 shareId sanitize(只允许字母数字与 _ -) |
| A 忘记关闭共享 | 会话条目带 TTL(默认 30 分钟),A 停止访问后自动过期;撤销共享即删除 sessions/ 对应文件 |
| B、C 的操作被审计为 A | 日志里区分 main/share 身份 |
| 内网系统不该被外网访问 | 部署层加 IP 白名单 / 基础认证 |
重要:会话共享本质是"借 A 的身份给 B、C 用",A 要对 B、C 的操作负责。生产环境务必加审计日志和撤销机制。
四、效果对比
| 维度 | 共享账号 | 复制 Cookie | 给每人开账号 | 会话共享 |
|---|---|---|---|---|
| 配置成本 | 低 | 中(每人复制) | 高(IT 审批) | 低(一条链接) |
| 续期 | 改密码全员失效 | Cookie 过期手动重发 | 各自续期 | A 重新登录自动同步 |
| 安全 | 差(明文密码) | 中(Cookie 易泄露) | 好 | 中(看 shareId 管理) |
| HttpOnly Cookie | N/A | 拿不到 | N/A | 能复用 |
| localStorage token | N/A | 手动复制 | 各自登录 | 自动复用 |
| 多人同时在线 | 可能冲突 | 各自独立 | 各自独立 | 支持 |
| 内网穿透场景 | 不适用 | 不适用 | 不适用 | 完美适配 |
五、应用场景
会话共享不是"白嫖会员"那么简单,真实场景包括:
- 远程协作:A 在公司内网登录了所有系统,B、C 在家通过 Web 内网穿透 + 会话共享直接接入协作,零配置
- 临时授权:外部合作伙伴需要临时访问内网某个系统,A 生成一个 share 链接发过去,用完撤销
- 演示环境:给客户演示内网系统,用 share 链接,不需要给客户开账号
- 多人共用付费服务:A 开了 ChatGPT Plus / 某设计工具会员,B、C 通过 share 链接复用(注意服务条款)
- 运维交接:A 离职前把会话 share 给 B,B 有时间窗口接管,期间 B 用 A 的身份完成交接
第 1、2、3 场景是内网穿透的核心用例------这才是会话共享真正的价值。
六、代码开源
完整实现已开源,包含:
- 域名编码(支持
-main-/-share-<id>后缀) - 服务端 Cookie/Authorization 采集与注入
- 客户端 localStorage 上报与回填(首次 3s + 定期上报 + sendBeacon 兜底)
- sessionStore 落盘持久化(原子写、TTL 自动清理、多进程共享)
- share 端 Set-Cookie 合并
GitHub:github.com/Stardust100...
欢迎 Star、Issue。如果你的团队也有远程协作 / 内网穿透 / 临时授权的需求,会话共享比开账号、复制 Cookie 都好用。
项目仅用于学习研究,会话共享涉及身份授权传递,请确保在授权范围内使用,勿用于违反法律法规或服务条款的用途。