先说我自己的处境:去年接了个内容代运营的活儿,一个人管三个平台,每个平台配两个号。每天最崩溃的不是写稿,而是换号——退出登录、清缓存、重新走验证码,一轮下来少说五分钟。一天光折腾账号就浪费半小时,更别提偶尔清过头,把不该丢的本地数据也清了。
后来我写了款油猴脚本,名字叫 AnMe,定位是通用多网站多账号切换器。装进篡改猴(Tampermonkey)之后,任何网站只要配置一次,就能把账号登录态像拍照一样存下来,之后点一下面板里的账号名,自动清空当前登录态、恢复目标账号的登录态、刷新页面,整套动作两秒内完成。这篇文章就聊清楚三件事:为什么值得用油猴脚本来做这件事、AnMe 的核心原理是什么、实际使用中哪些坑是我反复踩过的。
适合看这篇的人主要是三类:每天要管理多个账号的内容运营;需要快速切换测试账号的前端开发者;家里一台电脑多人共用、想让各人账号互不干扰的普通用户。如果你只是在某一个网站上需要偶尔换号,直接用无痕窗口或者多装一个浏览器内核可能更简单,不必上这种通用工具。但如果你和我一样,跨平台、跨站点、高频切换,那这篇的内容应该能帮你省下不少时间。
1. 换账号换到崩溃之后,我重新审视了市面上的三种方案
先说结论:多账号切换这个需求,市面上能用的方案其实不少,但没有一个能同时满足"轻量""通用""低成本"三个条件。我一开始也试过各种办法,最后才意识到油猴脚本才是这活儿最合适的容器。
1.1 浏览器多开:笨重且不彻底
最原始的办法是装多个浏览器内核,Chrome 管账号A,Edge 管账号B,Firefox 管账号C,互不干扰。这个思路简单,但问题很明显:内存开销是成倍翻的,三个浏览器同时开,16G 内存瞬间吃紧;每个浏览器还得分别装扩展、登录各种辅助服务;最难受的是 Cookie 隔离虽然做到了,但收藏夹、密码库、扩展插件这些数据也跟着分家了,体验非常割裂。
还有一个隐性问题:不少网站会做设备指纹类的风控,同一台机器换浏览器内核,User-Agent 变了,IP 没变,部分敏感场景反而更容易触发风控。实测下来,多开作为应急方案可以,作为长期工作流不可持续。
1.2 指纹浏览器:能力很强,但场景错位
指纹浏览器是这两年很火的方向,每个账号环境配独立的 Canvas 指纹、WebGL、时区、字体列表甚至 IP 出口,隔离做得确实是浏览器多开没法比的。但它的目标场景是跨境电商、广告投放矩阵这类"每个账号都要像完全不同的电脑"的严肃环境,价格也不便宜,按账号数计费,动辄每月几十上百。
如果我只是想在一个正经网站上快速切换自己名下的两个学习账号,用指纹浏览器属于杀鸡用牛刀。而且它的操作链路也重:打开软件、选环境、等启动、进页面,速度和轻量完全不搭边。更关键的是,指纹浏览器在有些场景下反而是多余的——网站根本没做那么细致的风控,你只需要换个登录态,它就够了。
1.3 自研浏览器插件:收益撑不起成本
正经一点的思路是写一个浏览器扩展,通过 cookies API 或 webRequest 来管理各站点的 Cookie。这个方案的隔离和恢复能力是最强的,因为扩展的权限远高于网页里的脚本,连 HttpOnly Cookie 都能读写。
但这带来两个现实问题:一是开发维护成本高,Chrome、Edge、Firefox 三端的扩展 API 有差异,打包、上架、审核都是时间;二是对于"我自己用"这种需求,一周迭代一个版本,每次更新都要重新加载扩展文件、重新确认权限,很烦。而且浏览器扩展的过审要求会限制一些功能,尤其是涉及账号数据管理这一类,隐私政策都要写清楚,个人项目做起来很吃力。
1.4 为什么最终落在油猴脚本上
油猴脚本的本质是运行在目标页面里的一段 JavaScript,但它比页面脚本多了几项特权:可以用 GM_setValue/GM_getValue 在扩展侧存数据、可以用 GM_xmlhttpRequest 发跨域请求、可以在所有匹配的网站上统一注入。这意味着"一套代码、任意网站通用、存储不受页面控制"这三个目标都能低成本实现。
对比一下几种方案的实际成本:
| 方案 | 开发成本 | 使用成本 | 隔离强度 | 通用性 | 适合人群 |
|---|---|---|---|---|---|
| 浏览器多开 | 零 | 高(内存/体验割裂) | 中 | 中 | 临时应急 |
| 指纹浏览器 | 零 | 高(付费/启动慢) | 强 | 强 | 跨境电商等严肃环境 |
| 自研扩展 | 高 | 中(更新/审核) | 强 | 中 | 有开发资源的团队 |
| 油猴脚本 | 低 | 低(装个脚本即可) | 中 | 强 | 个人和轻量团队 |
油猴脚本唯一的短板是没法碰 HttpOnly Cookie,这一点我在第 4 部分会详细说处理方案。但就性价比而言,对一个"通用多网站多账号切换器"来说,油猴脚本是当前最合适的宿主。AnMe 从立项到第一版可用的 Demo,我用了一个周末,这个速度在扩展方案里是不可想象的。
2. AnMe 的工作原理:给登录态拍快照,再整组恢复
理解 AnMe 只需要记住一句话:它本身不维护任何登录状态,只负责两件事——在捕获时把当前网站的登录态完整保存下来,在切换时把保存的登录态整组写回去。
2.1 核心思路:AnMe 不维护任何登录态
大多数网站的登录态由两部分组成:一组 Cookie 和一部分 localStorage 键值对。Cookie 里有会话 ID、用户 ID,localStorage 里可能有 token、用户设置、灰度开关。二者共同决定了"当前页面里坐着的是谁"。
所以 AnMe 的设计原则很简单:不对网站业务做任何假设,不关注你是谁、你有几个账号,只做"读取当前登录态"和"还原登录态"这两个原子操作。这样设计的好处是通用性强——任何网站,只要登录态是落在 Cookie 和 localStorage 里的,理论上都能接进来。坏处是它不负责账号的合规性判断,用户拿它做什么得自己把握。
这个思路有点像给系统盘做镜像:安装一个干净系统,装好所有软件,拍一个快照;出了问题,直接恢复快照,而不是重新装一遍系统。
2.2 站点配置与账号快照的数据模型
AnMe 的数据模型分成两层。
第一层是站点配置(SiteConfig),描述"这个网站该怎么处理"。核心字段是网站的匹配规则和 Cookie 域:
// AnMe 站点配置示意 const siteConfig = { key: 'platform_a', // 站点唯一标识 name: '平台A', match: ['*://*.platform_a.com/*'], // 页面地址匹配规则 cookieDomain: '.platform_a.com', // 切换时写 Cookie 的默认域 excludeCookieKeys: ['temp_*'], // 捕获时忽略的临时 Cookie useStorageSnapshot: true // 是否记录整站 localStorage };第二层是账号快照(AccountProfile),描述"某一个账号的登录态长什么样":
// AnMe 账号快照示意 const accountProfile = { id: 'snap_xxx_001', siteKey: 'platform_a', name: '运营-主号', cookies: [ { name: 'uid', value: '123456', domain: '.platform_a.com', path: '/', hostOnly: false } ], localStorageMap: { 'user_token': 'eyJhbGci...', 'user_info': '{"name":"main"}' }, createdAt: 1720000000000, updatedAt: 1720000000000 };关键的一点是 Cookie 不能只存名字和值,还要存 domain、path、hostOnly 这些元信息。原因很简单:写 Cookie 的时候如果不带上正确的 domain 和 path,浏览器会把它当成一个全新的 host-only Cookie,服务端可能不认,登录态就恢复不了。这个细节我是在第一版踩坑之后才补上的。
2.3 切换按钮背后到底执行了什么
当你在 AnMe 悬浮面板里点了一个账号,背后大概执行这样一段逻辑:
// AnMe 切换账号的核心简化流程(伪代码) function switchAccount(profile) { // 1. 把当前页面的可见 Cookie 全部置为过期 document.cookie.split(';').forEach(pair => { const name = pair.split('=')[0].trim(); document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`; document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=${currentSite.cookieDomain}`; }); // 2. 清空当前源下的 localStorage localStorage.clear(); // 3. 写入目标账号的 Cookie profile.cookies.forEach(c => { const ext = c.hostOnly ? `path=${c.path}` : `path=${c.path}; domain=${c.domain}`; document.cookie = `${c.name}=${c.value}; expires=${profile.expires.toUTCString()}; ${ext}; SameSite=Lax`; }); // 4. 写入目标账号的 localStorage Object.entries(profile.localStorageMap).forEach(([k, v]) => { localStorage.setItem(k, v); }); // 5. 等待浏览器落盘后刷新 setTimeout(() => location.reload(), 300); }注意这是高度简化的伪代码,真实实现里还要处理 HttpOnly、子域 Cookie、写失败检测等一堆边界情况。但从这个简化流程里已经可以看出,切换的本质无非是"清掉旧的状态,写回新的状态,刷新让服务端重新认证"。
有一点必须说清楚:油猴脚本只能操作"当前页面所在源"的数据。也就是说,AnMe 的切换按钮是在哪个网站上被打开的,就只能切哪个网站的账号,不存在一个面板切遍所有网站的设定。这和"通用"并不矛盾——通用体现在一套脚本适配所有网站的接入方式,而不是跨站操作。
3. 接入一个新网站的三步操作:登录、捕获、命名存档
工具设计得再巧妙,上手流程太复杂也没人用。AnMe 接入一个新网站,理想情况下只需要三步:先登录,再捕获,最后命名存档。
3.1 环境安装与脚本部署
基础环境没什么特殊的,先装好篡改猴扩展,然后从用户脚本市场安装 AnMe。装好之后,浏览器右上角的篡改猴图标里能看到 AnMe 的菜单入口,同时任意网页右下角会有一个可拖动的悬浮球。
这里建议确认一下篡改猴版本,AnMe 依赖的 GM_setValue、GM_registerMenuCommand 等 API 在正式版里都支持,但个别 Beta 版或第三方改版篡改猴对 GM 存储 API 的实现会有差异。我之前在一个 Chromium 内核的国产浏览器上遇到过 GM_setValue 静默失败的问题,排查了半天,最后发现是那个浏览器内置了一个不完整的篡改猴实现,换成官方版本就好了。
3.2 捕获账号快照的正确姿势
捕获快照的流程是:先正常打开目标网站,完成登录,等页面稳定下来(重点:等首页的接口请求结束,因为有些网站登录成功后会异步写一批 Cookie 和 localStorage),然后点开 AnMe 悬浮球,选择"捕获当前账号",输入账号别名,保存。
这里有几个实操细节值得注意:
- 不要在登录跳转过程中点捕获。有些网站登录后会自动重定向两三次,每一次跳转都会刷新 Cookie,跳转途中抓到的快照往往不完整。稳妥的做法是登录后停留 10 到 20 秒,确认个人信息加载出来了再捕获。
- 如果网站有明显的"记住我"选项,建议勾选。这样捕获到的 Cookie 一般带较长的过期时间,快照能持续使用更久。
- 捕获前可以先手动点一遍站内的主要入口,比如个人中心、消息、设置。目的不是真的浏览,而是触发站点把需要的登录态数据写入 localStorage,确保快照尽可能完整。
捕获完成后,AnMe 会弹出一个摘要:捕获了多少个 Cookie、多少个 localStorage 键、是否有检测到但无法读取的 HttpOnly Cookie。如果存在 HttpOnly Cookie 且数量很多,就要考虑用第 4 节的补充方案了。
3.3 多站点自动配对规则
AnMe 的悬浮面板不是固定显示一个站点的,它会根据当前页面 URL 自动匹配站点配置。比如你打开的是 platform_a.com 的页面,面板就显示 platform_a 的账号列表;打开的是 platform_b.com 的页面,面板自动切到 platform_b 的账号列表。
匹配的优先级是"完整域名优先于子域"。举个例子,site B 的匹配规则是 *://*.example.com/*,但你同时在 anme 的配置里给带后台路径的后台地址单独建了一个配置,那么打开后台地址时匹配的是更具体的那条规则,而不是通用匹配。这么做是为了应对一个站点里前后台登录态不同步的情况。
3.4 切错了怎么回滚
再好的工具也防不住手滑。AnMe 设计了两个保护机制:一是每个账号快照在切换前会自动保存一份"当前状态"到临时区,如果你发现切错了,可以切回去;二是面板里有一个"恢复上一个状态"按钮,相当于撤销。
这里我用的具体做法是:在每次执行切换前,先把当前页面的 Cookie 和 localStorage 快照存成一份 no-profile 临时快照,切换完成后再把这份临时快照挂在面板底部。用户点了撤销,就是拿这份临时快照执行一次反向切换。这个功能在第一版里没有,是我在测试账号 A 切到账号 B 之后,发现 B 有点问题、想切回 A 却发现 A 的登录态已经被覆盖时才补上的。虽然是事后亡羊补牢,但确实救了不少次场。
4. 实现过程中绕不开的四个硬骨头
把一个"看起来很简单"的多账号切换器真正做出来,和"能用在真实网站上"之间,隔着好几个大坑。这一节是我认为全文信息密度最高的部分,建议读完结合自己的站点实地验证。
4.1 HttpOnly Cookie 是抓不到也写不回的
HttpOnly 是 Cookie 的一个安全属性,设置了之后,JavaScript 的 document.cookie 就读不到它。服务端可以通过其余的会话信息来识别用户,但脚本这边对它是"两眼一抹黑"。
问题在哪个层面?捕获的时候,AnMe 读不到 HttpOnly Cookie,快照里自然就没有;切换的时候,就算服务端要求那个 HttpOnly Cookie 必须存在,脚本也没法把它的值写回去。于是会出现一种诡异的现场:Cookie 看起来"切过去了",但页面刷新后提示未登录,或者登录状态不完整。
我的实际处理方案是分三档:
- 对于大部分业务网站,HttpOnly Cookie 里通常只有会话 ID 类字段,而非 HttpOnly Cookie 和 localStorage 已经足够让服务端识别用户。这种情况不需要特殊处理,快照能正常使用。
- 如果关键会话 Cookie 是 HttpOnly 的,就需要"半自动导入"。具体做法是:在浏览器开发者工具里打开 Application 面板,查看对应站点所有 Cookie,把 HttpOnly 项的值手动复制进 AnMe 的"补充 Cookie 导入"窗口。这个操作只做一次,之后切换时会自动带上。
- 如果网站把所有关键 Cookie 都设置成 HttpOnly,那这个站点确实不适合全自动切换,AnMe 会给出明确提示,建议改用其他方案。硬要自动化的收益已经不划算了。
我知道这算一个能力边界,所以在 AnMe 的说明里也写得很清楚:它不是一个能绕过所有网站安全策略的工具,它只是在网站允许脚本操作的那部分存储上做文章。
4.2 子域 Cookie 的 Domain 处理
很多大型网站的登录态分布在多个子域:www.example.com 的页面里放着前端生成的 Cookie,api.example.com 的会话 Cookie 则带 Domain=.example.com 的父域属性。前者是 host-only Cookie,只能被当前子域访问;后者是 domain Cookie,所有 *.example.com 都能访问。
AnMe 的捕获逻辑会记录每个 Cookie 的 domain 和 hostOnly 属性。写回的时候,host-only Cookie 不带 domain 参数,domain Cookie 带原始 domain 参数。我第一次写切换逻辑时偷懒统一用了站点配置里的 cookieDomain,结果那些原本不打算分享给父域的 Cookie 全部变成了 domain Cookie,站点行为立刻出现异常,比如购物车数据串号。排查这种诡异问题最费时间,因为页面看起来几乎正常,只是某处行为不对。
所以一定不要想当然地觉得"都是 Cookie,统一处理就行"。
4.3 localStorage 多键恢复的顺序敏感
localStorage 不是所有站点都用了,但用了的那些站点,恢复时往往有顺序问题。有些站点会先读一个配置键判断用户类型,再根据用户类型去读另一个键;如果先恢复了第二个键、第一个键还没写回来,页面可能直接落在一个错误的初始状态。
最省心的方案是整站快照:捕获时记录当前源下所有 localStorage 键值对,恢复时先 clear 再全量写回。这样每个键都在,顺序再敏感也能一次性恢复到一致状态。代价是快照体积变大,但 localStorage 的单站点数据量通常也就几 KB 到几十 KB,完全可接受。
实际操作中我还加了一个排除名单功能,允许用户指定哪些键不参与快照。原因是有些站点会把浏览器性能统计数据写进 localStorage,这些数据无关登录态,但会在每次捕获时变化,造成账号快照反复变更、难以比对。排除掉无关键之后,捕获时就能稳定生成"同样的账号多次捕获结果一致"的快照。
4.4 从写入到刷新之间的时序控制
Cookie 和 localStorage 的写入在 JavaScript 里都是同步提交的,理论上写完立刻刷新没问题。但我在实际测试中发现,部分网站会在页面卸载前把内存里的 token 再写回 localStorage,这可能覆盖刚恢复的数据。典型场景是 SPA 应用,框架在 beforeunload 生命周期里执行持久化操作。
处理方式是给刷新加一个 300ms 左右的延迟,让页面不再触发额外的写操作后再 reload。300ms 这个值是测试出来的经验值,太短容易碰上异步写入没完成,太长影响体验。
还有一个细节:刷新要用 location.reload(),不要在脚本里用 history.go() 或者跳转 URL。因为部分单页应用会用 History API 自己做路由,reload 是唯一能保证完整重新加载页面上下文的方式。
5. 安全边界:本地存储不代表可以裸奔
账号切换器这东西,本质上在本地保存了一堆网站的登录凭据。数据安全必须提前想清楚。我的立场是:AnMe 只做本地存储,不上传任何数据,但你也不能因为"在本地"就觉得绝对安全。
5.1 账号数据究竟存在哪里
篡改猴扩展在 Chromium 浏览器里使用 IndexedDB 存储 GM_setValue 写入的数据,在 Firefox 里则偏向使用 extension.storage.local。共同点是数据都保存在浏览器 profile 目录下,和你的其他扩展数据放在一起。
这些数据在磁盘上是明文,或者至少是普通用户可以直接读取的格式。也就是说,任何一个能访问你电脑开着的浏览器 profile 目录的进程,理论上都能把 AnMe 存储的账号快照读出来。对于想要深度体验的用户,这个概念要先接受。
正因如此,AnMe 的默认设计里没有任何云端同步,账号快照只存在于当前浏览器 profile 的存管空间里。想换到另一台电脑继续用?官方不提供同步通道,请自己导出导入快照文件。虽然麻烦一点,但这比把敏感数据交给一台云服务器稳妥得多。
5.2 凭据混淆与主密码方案
为了降低"明文裸奔"的风险,AnMe 里内置了一个可选的"凭据混淆"开关。开启后,捕获到的 Cookie 值在写入存储之前会经过一层混淆处理。它的效果是让直接查看 IndexedDB 的人看不到明文,但严格来说这只能防君子不防小人,因为混淆逻辑和密钥都在脚本代码里,花点时间逆向就能还原。
如果你管理的账号确实重要,我建议开启"主密码锁定"模式。原理是:你用主密码对账号快照做一次真正的加密,脚本运行时在内存里持有解密后的快照,切换时直接使用。脚本刷新页面后需要重新输入主密码才能继续看到账号列表。这个模式在便捷性上有损耗,但对安全敏感的使用场景是值得的。
这里要提醒一点:主密码如果忘了,所有快照都无法恢复,没有找回机制。我建议在开启主密码前先导出一份未加密的快照文件,放在加密的移动存储设备里,作为应急恢复通道。
5.3 哪些场景容易泄露,怎么规避
最容易出问题的场景不是黑客攻击,而是日常习惯,比如:
- 在公共电脑上用了 AnMe,走之前没有清理快照。解决方案是公共设备改用临时访客模式,或者退出 Windows 账号后再离开。
- 浏览器 profile 被同步类软件备份到云端。这就意味着你的账号快照跟着躺在了云端,而你没有主动选择过。方案是把该软件的对浏览器 profile 的备份目录排除掉。
- 快照文件转发给朋友。国内市场很多使用者会互相分享配置和快照,但这等于把登录态直接给了对方,换了任何解释口径都不是安全行为。
6.3 家庭共用浏览器的账号隔离
第三类场景是家庭共用一台电脑。之前不是没法一起用,而是每次切换账号都搞得鸡飞狗跳:老婆退出自己的邮箱,我再登录我的,等我用完她又得登录一遍。
AnMe 在电脑上的表现是:给常用网站配置了两套账号快照,一人一套。谁要用,点悬浮球切到自己账号,用完再切回来。家里的浏览器是 Chrome,之前还担心家庭成员误操作删掉快照——我在 AnMe 里把另一个账号的快照设置成"只允许手动覆盖",默认切换不会改动原始快照,防住这一层误操作。
6.4 版本迭代里的关键取舍
这批实测直接催生了 AnMe 的三次重要改动。第一次是加入"整站 localStorage 快照",因为之前只抓关键键值,导致某个平台切过去白屏;第二次是加入临时快照和撤销机制,因为误操作后想恢复却没入口;第三次是把冷配置从写死在脚本里改成通过开发者界面维护,因为不同的人接入的站点实在太多,光靠改代码再发布新版本,维护成本扛不住。
后来我再想,如果一开始就把"通用性"放在最高优先级,而不是"最小实现",这三次大改里至少有一半是提前能预见的。这个教训是我在实践里学到的,写出来当参考:做工具类脚本,通用配置比快速实现重要,撤销机制比操作提示重要,安全兜底比功能炫酷重要。
做完这些事,AnMe 才真正从一个"自己用的小脚本"变成了能稳定推荐给别人用的通用多网站多账号切换器。
最后再分享一点个人的使用习惯:我把 AnMe 的悬浮球固定在最右侧边缘,平时完全不挡页面内容;正式切号前如果页面上有编辑到一半的草稿,我会先复制到剪贴板或者本地文档,因为刷新动作会丢弃所有未提交的表单。这些小事不写在功能文档里,但实际用的频率非常高,希望对正准备用的你也有参考价值。