一人VIP账号多人共用:Web 代理里的会话共享怎么实现

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(只允许字母数字与 _ -,限长),杜绝路径穿越。

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 看到已登录的页面

三、关键技术点

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。

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 手动复制 各自登录 自动复用
多人同时在线 可能冲突 各自独立 各自独立 支持
内网穿透场景 不适用 不适用 不适用 完美适配

五、应用场景

会话共享不是"白嫖会员"那么简单,真实场景包括:

  1. 远程协作:A 在公司内网登录了所有系统,B、C 在家通过 Web 内网穿透 + 会话共享直接接入协作,零配置
  2. 临时授权:外部合作伙伴需要临时访问内网某个系统,A 生成一个 share 链接发过去,用完撤销
  3. 演示环境:给客户演示内网系统,用 share 链接,不需要给客户开账号
  4. 多人共用付费服务:A 开了 ChatGPT Plus / 某设计工具会员,B、C 通过 share 链接复用(注意服务条款)
  5. 运维交接: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 都好用。

项目仅用于学习研究,会话共享涉及身份授权传递,请确保在授权范围内使用,勿用于违反法律法规或服务条款的用途。

相关推荐
用户老李16 分钟前
无线电自学教程 05:第一台收音机——从矿石机到 SDR
前端
IPIPD23 分钟前
Node.js请求遇到503怎么重试?先解析Retry-After,再做有上限退避
前端
葡萄城技术团队30 分钟前
dsh-spreadjs-editor 的集成链路:把 SpreadJS Designer 嵌进 Agent 工作台
前端
FlyWIHTSKY1 小时前
vue2和vue3有什么区别,用法上有哪些区别
前端·javascript·vue.js
码云之上1 小时前
10 颗一模一样的「Edit」:中后台 Web Agent 怎么找到对的那一行
前端·人工智能·浏览器
量化分析码农2 小时前
【Python量化多因子实战 04】因子暴露不是 alpha:市值与行业中性化实战
前端·后端
不可能片场2 小时前
Electron 发布 正文写入的三条通道与读回口径
前端·electron
jerrywus3 小时前
让 AI 帮你部署 Vercel,报错了也不用手动搬日志
前端·agent·mcp
补码缩写3 小时前
11 行 Python 拆 PNG 块,解压后的字节数能手算
前端