1. 这个脚本到底解决了什么问题
第一次看到“Exhentai-Shared-Account”这个标题,很多人脑子里冒出来的第一个词大概是“共享账号”。没错,它的核心逻辑就是字面意思——把一组公共可用的登录凭据,通过浏览器脚本的方式自动写入到目标站点的 Cookie 里,让访问者不需要手动输入账号密码,就能直接进入里站浏览内容。整个项目本质上是一个跑在 Tampermonkey(篡改猴)里的 JavaScript 脚本,依赖浏览器扩展注入页面,操作对象是 Cookie。
先把话说在前面:这篇文章只讨论脚本本身的技术实现思路、Cookie 机制的原理、以及这类“共享凭据自动注入”方案在工程上会遇到哪些坑。至于账号从哪来、共享行为是否合规、目标站点的服务条款怎么规定,这些不在技术讨论范围内,需要读者自行判断。我写这篇东西的出发点很简单——这类脚本涉及的知识点其实相当密集:Cookie 的作用域与生命周期、Tampermonkey 的注入时机、跨子域写入的限制、脚本被浏览器安全策略拦截的排查方法。把这些讲透,比单纯丢一个安装链接有价值得多。
适合谁来读?如果你满足下面任意一条,这篇内容对你就有用:
- 你写过或想写 Tampermonkey 脚本,但对
@match、@run-at、GM_cookie这些指令的实际行为一知半解; - 你遇到过“脚本装了但没生效”“Cookie 写进去了但刷新就没了”“登录状态时有时无”这类问题,想搞清楚底层原因;
- 你想理解浏览器 Cookie 的 Domain、Path、SameSite、HttpOnly 这几个属性到底怎么影响脚本操作;
- 你对“共享凭据自动注入”这种模式的技术架构感兴趣,想评估它的稳定性和风险点。
我前后折腾过好几个版本的类似脚本,踩过的坑包括但不限于:Cookie 写入了但 Domain 不匹配导致浏览器直接丢弃、document.cookie在 HttpOnly 面前完全失效、Tampermonkey 的沙箱环境和页面环境对 Cookie 的可见性不一致、脚本执行时机太早导致页面已经发起了未携带凭据的请求。这些问题在官方文档里往往一笔带过,但在实际使用中每一个都能让你卡上半天。下面我把整个方案拆开,从设计思路到实操细节,再到排查手册,尽量讲清楚。
2. 脚本整体设计与核心思路拆解
2.1 为什么选择 Tampermonkey 而不是独立程序
这类“自动登录/凭据注入”需求,技术上至少有四种实现路径:浏览器扩展、独立桌面程序、命令行工具配合系统级 Cookie 数据库操作、以及用户脚本(Userscript)。这个项目选了用户脚本,背后是有明确取舍的。
独立桌面程序(比如用 Python + Selenium)的问题是重——你得装运行环境、装浏览器驱动、每次启动要等浏览器实例起来,而且它操作的是一个全新的浏览器会话,跟你日常用的浏览器完全隔离,Cookie 不共享。命令行工具直接改浏览器 Cookie 数据库(Chrome 的CookiesSQLite 文件)看起来优雅,但 Chrome 在运行时会锁住这个文件,你还得先关浏览器,而且新版 Chrome 对 Cookie 值做了加密(Windows 上绑定 DPAPI,macOS 上绑定 Keychain),解密成本很高,跨平台极不友好。
浏览器扩展能做到最深度的控制,但开发、打包、上架、维护一套流程下来,为了一个“写几个 Cookie”的需求实在不划算。用户脚本正好卡在中间:它借助 Tampermonkey 这个宿主扩展获得了接近原生扩展的 API 能力(比如GM_cookie、GM_setValue),同时开发成本极低——一个.user.js文件,改完刷新页面就生效,不需要打包和签名。
提示:Tampermonkey 提供的
GM_cookieAPI 是这类脚本能绕开document.cookie限制的关键。原生document.cookie无法读写 HttpOnly 的 Cookie,也无法跨域操作,而GM_cookie在用户授权后可以做到这两点。这是选型时最容易被忽略、但实际最要命的一个差异点。
2.2 核心流程:从脚本注入到凭据生效
整个脚本的工作流可以拆成五个阶段,理解这五个阶段是排查一切问题的前提:
- 匹配与注入:Tampermonkey 根据脚本头部的
@match或@include规则,判断当前页面 URL 是否命中,命中则在指定时机(@run-at)把脚本注入页面。 - 凭据读取:脚本从内置的常量、远程配置接口、或者
GM_setValue存储中拿到那组共享账号对应的 Cookie 键值对。 - Cookie 写入:通过
GM_cookie.set或document.cookie把键值对写入目标域。 - 状态校验:写入后发起一次探测请求(或读取页面特征元素),确认登录态是否真的生效。
- 失败重试与降级:如果校验失败,脚本可能尝试备用凭据、调整写入参数、或提示用户手动干预。
这五步里,第三步和第四步是绝大多数“脚本不生效”问题的根源。写入这一步,很多人以为document.cookie = "key=value"就完事了,实际上浏览器会根据当前页面的域、路径、协议、以及 Cookie 自身的属性做一系列校验,任何一项不匹配都会静默丢弃——注意是静默,浏览器不会报错,你只会发现“怎么没写进去”。
2.3 共享凭据模式固有的稳定性难题
必须承认,“共享账号”这个模式在工程上有天然缺陷,理解这些缺陷能帮你建立合理的预期,也能帮你在脚本设计时做出更稳健的取舍。
第一个难题是凭据的时效性。共享账号的密码或会话令牌可能被服务端定期轮换、被其他使用者触发风控、或者因为并发登录数超限而被临时封禁。脚本本身无法创造有效的凭据,它只能搬运。所以这类脚本的可用性高度依赖凭据源的维护频率。
第二个难题是并发冲突。同一个账号被多人同时使用时,服务端可能检测到异常(比如同一账号在短时间内从多个地理位置发起请求),从而触发验证或封禁。脚本层面能做的缓解很有限,通常只能靠“错峰”或者“多凭据轮换”来降低单账号压力。
第三个难题是Cookie 与登录态的绑定关系。很多站点不是简单校验一个 Cookie 值,而是把会话 ID 和服务端的会话记录绑定。你光有 Cookie 字符串还不够,服务端那边的会话记录必须还有效。这就解释了为什么有时候“Cookie 明明写对了,但还是未登录”——服务端会话已经过期了。
理解了这三点,你就明白为什么这类脚本的 README 里总会写“不保证长期可用”。这不是作者偷懒,而是模式本身的限制。
3. Cookie 机制与脚本注入的关键细节
3.1 Cookie 的五个核心属性,一个都不能错
要让脚本写入的 Cookie 真正生效,必须同时满足五个属性的约束。我用一个表格把它们列清楚,这是排查问题的第一手资料:
| 属性 | 作用 | 脚本操作时的常见错误 |
|---|---|---|
| Domain | 指定 Cookie 对哪些域可见 | 写成www.example.com但目标请求发往example.com,导致不匹配 |
| Path | 指定 Cookie 对哪些路径可见 | 写成/forum但实际请求路径是/,Cookie 不携带 |
| Expires/Max-Age | 过期时间 | 不设置则成为会话 Cookie,关浏览器即失效 |
| Secure | 仅通过 HTTPS 传输 | 在 HTTP 页面写入 Secure Cookie 会被拒绝 |
| SameSite | 跨站请求是否携带 | 设为Strict时,从外部链接跳转进来首次请求不带 Cookie |
这里最容易翻车的是 Domain。假设目标站点是exhentai.org,但登录接口实际在forums.exhentai.org,那么 Cookie 的 Domain 必须写成.exhentai.org(前面带点表示包含所有子域),而不是exhentai.org或forums.exhentai.org。写错了,浏览器在发送请求时就会认为“这个 Cookie 不属于当前请求的域”,直接不携带。
注意:Domain 前面那个点在现代浏览器里的语义已经弱化了。RFC 6265 规定,
Domain=.example.com和Domain=example.com在大多数情况下等价,都会匹配所有子域。但为了兼容老代码和避免歧义,很多脚本仍然保留前导点。真正会导致失败的是把 Domain 写成某个具体子域,却期望它在另一个子域生效。
3.2 document.cookie 与 GM_cookie 的能力边界
这是脚本开发里最需要搞清楚的一组对比。很多人写脚本时习惯性用document.cookie,结果遇到 HttpOnly 就彻底没辙。
document.cookie的能力边界:
- 只能读写当前页面域下的 Cookie,无法跨域;
- 无法读取HttpOnly标记的 Cookie(这是浏览器故意的安全设计,防止 XSS 窃取会话);
- 写入时受当前页面的协议、路径限制;
- 读取时返回的是所有非 HttpOnly Cookie 拼接成的字符串,需要自己解析。
GM_cookie的能力边界(需要 Tampermonkey 授权):
- 可以读写任意域的 Cookie,只要用户在 Tampermonkey 设置里授予了权限;
- 可以读取HttpOnlyCookie(这是它最大的价值);
- 提供结构化的
list、set、delete接口,不用自己解析字符串; - 但它是异步 API,必须用回调或 Promise 处理,写起来比
document.cookie啰嗦。
对于“共享账号注入”这个场景,如果目标站点的会话 Cookie 是 HttpOnly 的(绝大多数正规站点都是),那么document.cookie方案从根上就行不通,必须用GM_cookie。这也是为什么很多早期版本的脚本在站点更新后就失效了——站点把会话 Cookie 改成了 HttpOnly,脚本的写入方式没跟着变。
3.3 脚本注入时机:@run-at 的选择逻辑
Tampermonkey 的@run-at指令决定了脚本在页面生命周期的哪个点执行,常见取值有:
document-start:DOM 还没开始构建,页面脚本尚未执行;document-end:DOM 构建完成,但外部资源(图片、样式)可能还在加载;document-idle:页面完全加载完毕,所有资源就绪。
对于凭据注入类脚本,理论上越早越好——因为如果页面在脚本写入 Cookie 之前就发起了登录校验请求,那次请求就是未登录状态,页面可能已经渲染成“未登录”的样子了。所以理想选择是document-start。
但document-start有个坑:此时GM_cookie的异步写入可能还没完成,页面脚本就已经跑了。这就产生了一个竞态条件。稳健的做法是:在document-start阶段同步地拦截关键请求(如果脚本有能力的话),或者接受“首次加载可能未登录,写入完成后自动刷新一次”的降级方案。很多成熟脚本采用的就是“写入后检测,未生效则location.reload()”的策略。
提示:如果你发现脚本“第一次打开没登录,刷新一下就好了”,基本可以确定是注入时机和异步写入的竞态问题。这不是 bug,而是这类方案的固有特性,可以通过在脚本里主动触发一次刷新来改善体验。
4. 实操过程与核心环节实现
4.1 环境准备与脚本安装
先把基础环境搭起来。整个过程分三步,我按实际操作顺序写:
- 安装浏览器扩展:在 Chrome、Edge、Firefox 等主流浏览器的扩展商店里搜索 Tampermonkey(中文名“篡改猴”),安装并启用。安装后浏览器工具栏会出现一个黑色图标,点击能看到管理面板。
- 开启开发者模式:部分浏览器(尤其是 Chrome 的新版本)需要在扩展管理页面打开右上角的“开发者模式”,否则 Tampermonkey 无法正常注入脚本。这一步经常被忽略,导致“脚本装了但完全没反应”。
- 导入脚本:打开 Tampermonkey 管理面板,选择“添加新脚本”,把
.user.js的内容粘贴进去,或者直接把.user.js文件拖进浏览器窗口,Tampermonkey 会自动识别并弹出安装确认页。
安装确认页会列出脚本声明的权限,比如GM_cookie、GM_setValue、以及@match的域名列表。这里要仔细看一眼——如果脚本声明了GM_cookie但你没在 Tampermonkey 的“设置 → 高级 → 安全”里允许相应权限,脚本运行时会拿不到 Cookie 读写能力。
4.2 脚本头部配置的逐行解读
一个典型的凭据注入脚本,头部大概长这样(这是基于常见实践的示例结构,不是某个具体脚本的原文):
// ==UserScript== // @name Shared Account Auto Login // @namespace local.shared.login // @version 1.0.0 // @description 自动注入共享凭据 // @match https://target-site.example/* // @grant GM_cookie // @grant GM_setValue // @grant GM_getValue // @run-at document-start // @connect config-source.example // ==/UserScript==逐行说明关键项:
@match:决定脚本在哪些页面注入。注意https://target-site.example/*只匹配该域下的所有路径,但不匹配子域。如果要覆盖子域,得写成https://*.target-site.example/*。@grant:声明需要使用的 Tampermonkey 特权 API。每用一个都要声明,漏声明会导致该 API 在脚本里是undefined。@run-at document-start:尽早注入,理由前面讲过。@connect:如果脚本要从远程拉取凭据配置,必须在这里声明目标域名,否则 Tampermonkey 会拦截跨域请求。
4.3 Cookie 写入的核心代码与参数计算
写入环节是整个脚本的心脏。用GM_cookie写入的典型代码结构如下:
function injectCookies(cookieList) { return new Promise((resolve, reject) => { let pending = cookieList.length; if (pending === 0) return resolve(); cookieList.forEach(function (item) { GM_cookie.set({ name: item.name, value: item.value, domain: item.domain, // 例如 ".target-site.example" path: item.path || "/", secure: true, httpOnly: item.httpOnly || false, expirationDate: item.expirationDate || (Date.now() / 1000 + 86400 * 7) }, function (err) { if (err) console.error("写入失败:", item.name, err); pending--; if (pending === 0) resolve(); }); }); }); }几个参数的计算和选择逻辑值得展开:
expirationDate 的取值。这个字段是 Unix 时间戳(秒),不是毫秒。Date.now()返回的是毫秒,所以必须除以 1000。上面代码里86400 * 7表示 7 天。为什么设 7 天而不是永久?因为共享凭据本身可能几天就失效了,设太长没意义,反而可能在凭据失效后留下一个“看起来还在但实际无效”的 Cookie,干扰排查。设太短又会导致频繁重新注入。7 天是个经验值,你可以根据凭据源的更新频率调整。
domain 的确定方法。不要凭感觉写。正确做法是:在目标站点正常登录一次,打开浏览器开发者工具的“应用 → Cookie”面板,看真实会话 Cookie 的 Domain 字段是什么,照着抄。如果真实 Cookie 的 Domain 是.target-site.example,你就写.target-site.example;如果它没有 Domain(表示仅当前精确域),你就留空或写当前域。
httpOnly 的取舍。如果目标站点的会话 Cookie 是 HttpOnly 的,你写入时也应该设httpOnly: true,否则可能出现“脚本写的 Cookie 和站点期望的 Cookie 属性不一致”导致的行为异常。但要注意,设了 HttpOnly 之后,你自己用document.cookie就读不到了,调试时得用GM_cookie.list。
4.4 写入后的校验与自动刷新
写完 Cookie 不代表登录态就生效了。稳健的脚本会做一次校验,我常用的校验逻辑有两种:
第一种是特征元素检测。登录后页面通常会显示用户名、头像、或者某个只有登录用户才能看到的按钮。脚本在写入 Cookie 后等待一段时间(比如 1.5 秒),然后查询这个特征元素是否存在。存在则说明成功,不存在则触发刷新或提示。
function verifyLogin() { const marker = document.querySelector(".user-avatar, .logout-link"); return !!marker; } setTimeout(function () { if (!verifyLogin()) { console.warn("登录态未生效,尝试刷新"); location.reload(); } }, 1500);第二种是探测请求。用fetch请求一个只有登录用户才能访问的接口,看返回状态码。这种方式更准确,但需要知道具体的接口地址,且可能触发额外的服务端日志。
注意:自动刷新要加防抖。如果凭据本身已经失效,脚本会陷入“写入 → 校验失败 → 刷新 → 再写入 → 再失败”的死循环。务必用
sessionStorage或GM_setValue记录刷新次数,超过阈值就停止并提示用户。
5. 常见问题与排查技巧实录
5.1 脚本完全不生效的排查顺序
遇到“装了脚本但页面毫无变化”,按下面这个顺序排查,基本能定位到问题:
| 排查项 | 检查方法 | 典型原因 |
|---|---|---|
| 脚本是否注入 | 打开控制台看有没有脚本的日志输出 | @match规则不匹配当前 URL |
| 权限是否授予 | Tampermonkey 面板看脚本的权限状态 | GM_cookie未授权 |
| 开发者模式 | 浏览器扩展页看开关 | Chrome 新版默认关闭 |
| 注入时机 | 看日志时间戳与页面加载的关系 | @run-at太晚,页面已渲染 |
| Cookie 是否写入 | 用GM_cookie.list打印当前 Cookie | Domain/Path 不匹配被丢弃 |
我踩过最隐蔽的一个坑是@match的协议问题。脚本写的是http://,但站点已经全站跳转https://,结果脚本永远不注入。这种问题在控制台里没有任何报错,只能靠肉眼比对 URL。
5.2 Cookie 写入成功但登录态不生效
这是最高频的问题,原因通常有三类:
第一类:服务端会话已失效。Cookie 字符串本身是有效的格式,但服务端对应的会话记录已经过期或被清理。这种情况下,无论你怎么写 Cookie 都没用,只能换一组凭据。判断方法:用同样的 Cookie 在无痕窗口手动测试,如果手动也不生效,就是凭据问题,不是脚本问题。
第二类:Cookie 不完整。很多站点的登录态不止一个 Cookie,可能是session_id+csrf_token+user_prefs的组合。脚本只写了其中一个,自然不生效。解决办法是抓取一次完整登录后的所有 Cookie,全部注入。
第三类:属性不匹配。前面讲过的 Domain、Path、Secure、SameSite 任何一个不对都会导致 Cookie 不被携带。用开发者工具的“网络”面板看实际请求头里的Cookie字段,对比你写入的 Cookie,就能发现差异。
5.3 共享凭据的稳定性维护经验
用了这类脚本一段时间后,我总结出几条维护经验,都是实打实踩出来的:
- 准备多组备用凭据。脚本里内置一个凭据数组,主凭据校验失败时自动切换到备用。这能显著提升可用性,因为单组凭据的失效是常态。
- 记录失效时间点。用
GM_setValue记录每次凭据失效的时间,观察规律。如果发现某组凭据总是在固定时间失效,可能是服务端的定时清理策略,可以据此调整更新频率。 - 不要在高峰期使用。共享账号的并发压力在特定时段会很高,触发风控的概率也大。错峰使用能降低被临时限制的风险。
- 定期清理残留 Cookie。凭据切换时,旧凭据的 Cookie 如果没清干净,可能和新凭据冲突。切换前先用
GM_cookie.delete清一遍目标域的相关 Cookie。
5.4 浏览器安全策略带来的额外限制
现代浏览器对第三方 Cookie 的限制越来越严,这对脚本有直接影响。如果你的脚本需要在 A 站点写入 B 站点的 Cookie(跨站场景),可能被浏览器的“阻止第三方 Cookie”策略拦截。Tampermonkey 的GM_cookie在一定程度上能绕过这个限制,但前提是用户授予了相应权限,且浏览器的“隐私沙盒”相关设置没有把它彻底堵死。
另外,Chrome 的“增强型安全浏览”和某些企业策略会限制用户脚本的执行。如果你在公司电脑上发现脚本怎么都不生效,先确认是不是被组策略限制了。
6. 我对这类方案的真实看法
折腾了这么多版本,我最深的体会是:这类脚本的技术门槛其实不高,难的是对 Cookie 机制的理解深度和排查问题的耐心。真正让人卡住的从来不是“怎么写代码”,而是“为什么写了没生效”——而答案往往藏在浏览器的某个静默行为里。
如果你打算自己写一个类似的脚本,我的建议是先把GM_cookie的官方文档通读一遍,然后在目标站点上手动登录一次,用开发者工具把真实 Cookie 的每一个属性都记下来,照着这个“标准答案”去写注入逻辑。不要凭想象填 Domain 和 Path,这两个字段是翻车重灾区。
最后分享一个调试小技巧:在脚本里加一个开关,通过 URL 参数控制是否输出详细日志。比如访问target-site.example/?debug=1时打印所有 Cookie 操作,平时则静默运行。这样既不影响日常使用,又能在出问题时快速定位。我用这个方法省下了大量“盲猜”的时间。