- JavaScript 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
定位:newValidate 是跨域请求(mappv5 -> gateway),默认 referrer policy strict-origin-when-cross-origin 只发 origin,路径丢失 -> 解析不到 appId。 - 新增 CX_LASTSID:从 getchargearticle / app_api/views/* / purchase/article/get/* 的 URL 或请求体嗅探明文文章 id,90 秒内有效,作为 newValidate 的兜底来源 - 模块 pattern 增加 app[_-]api/views/ 与 purchase/article/get/,并打开 debug=true 让 console.log 进入 Surge 请求备注,便于远程排查 - README/API.md 记录该 Referer 行为 |
||
| caixin-unlock.js | ||
| caixin-unlock.sgmodule | ||
| README.md | ||
caixin-unlock
财新网(caixin.com)会员内容解锁 —— Surge 模块 + 单文件脚本。
用一个会员账号的 Cookie,在任意浏览器 / 设备上拿到该账号本来就有权限的内容:网页文章注入全文并隐藏收费墙、App 内嵌正文页同样注入、网页音频由试听换成完整音频。
只做"把配置里那个账号的凭据带到每个请求上"这一件事,不消费赠阅次数或单次阅读卡,不写任何账号状态。
安装
Surge → 模块 → 从 URL 安装:
https://git.szzn.dev/congshuhao/caixin-unlock/raw/branch/main/caixin-unlock.sgmodule
安装后不用改模块正文,直接打开该模块的参数界面填写凭据(模块用 Surge 的 #!arguments 参数表,改完立即生效、不用重装)。需要 Surge iOS 5.10.0+ / Mac 5.5.0+。
模块里的 script-path 写的是绝对 URL(不是相对路径),Surge 会自行下载并缓存脚本,script-update-interval=3600 表示最多 1 小时拉一次更新。
配置
模块暴露 6 个参数:
| 参数 | 默认 | 说明 |
|---|---|---|
uid |
空 | SA_USER_UID,账号 ID,同时是网页接口 X-Sign 的 uid |
code |
空 | USER_LOGIN_CODE,登录会话码 |
unit |
1 |
SA_USER_UNIT,必须与 Cookie 里的值一致,填错会静默失效 |
deviceType |
4 |
SA_USER_DEVICE_TYPE,必须与 Cookie 里的值一致(常见 1=iPhone、4=iPad) |
appinfo |
空 | 可留空。留空时脚本用 uid/code/unit/deviceType 按 DES-ECB("1234abcd") 现算(与 App 同规则);填了就直接复用 |
debug |
0 |
填 1 在 Surge 日志里打印解锁过程 |
对应的模块声明与替换:
#!arguments=uid,code,unit:1,deviceType:4,appinfo,debug:0
...
argument=uid={{{uid}}}&code={{{code}}}&unit={{{unit}}}&deviceType={{{deviceType}}}&appinfo={{{appinfo}}}&debug={{{debug}}}
参数里没有空格也没有分号,脚本自己拼成 Cookie,避免 Surge 解析 [Script] 行时的歧义。
四个必需值必须与该账号注册时一致,改成任意值服务端一律判为无权限(网页 status: -7、App power: 0)。uid/code/unit/deviceType 从浏览器登录 caixin.com 后的 Cookie 里取。
注意:
- 参数值里不要出现逗号(
,):Surge 用逗号分隔[Script]行的选项,逗号会把argument截断。四个必需字段本来就不含逗号。 - 也可以改用
$persistentStore存整串 Cookie:key =CX_COOKIE,脚本会自动读取(参数留空时生效)。 - 两者都为空时回落到请求自带的 Cookie(浏览器已登录的话无需配置)。
- 旧版 Surge 不认识
#!arguments时占位符不会被替换,脚本会拿到{{{uid}}}字面量并当作无效凭据原样放行(安全失败,不会误注入)。
覆盖范围
一条 [Script] 正则覆盖 5 条链路,脚本按 $request.url 分发:
| # | 链路 | 做法 |
|---|---|---|
| 1 | 网页文章页 *.caixin.com/YYYY-MM-DD/{id}.html |
调 newauth/checkAuthByIdJsonp 注入 #Main_Content_Val,隐藏 #chargeWall / #pay-box / #loadinWall |
| 2 | App 内嵌正文页 mappv5.caixin.com/articlev5/{a}/{appId}.html |
注入 .cx-cons 与 articleProperties.content,并记录 appId→source_id |
| 3 | app-api/auth/newValidate 响应 |
无权限时用会员身份重放,整体替换响应 |
| 4 | app-api/userAuth/checkUserAndLoginRead 响应 |
同上 |
| 5 | newauth/checkAuthByAudioIdJsonp 响应 |
只有试听时重放,换成带 token 的完整音频地址 |
| 6 | app_api/article/getchargearticle 响应 |
App 打开文章时会带明文 source_id 调它,记录 appId→source_id 映射与"当前文章" |
| 7 | app_api/views/*、purchase/article/get/* |
从 URL 或请求体里嗅探明文文章 id,作为 newValidate 的兜底来源 |
MITM 需要 *.caixin.com。
排查(解锁不生效时按顺序看)
脚本每次被触发都会往 Surge 的脚本日志里写一行(注意:Surge 只记录 console.log 的第一个参数,所以脚本把整条消息拼成了一个字符串),形如:
[cx-unlock] web-article | arg=589B | cred=uid=y user_login_code=y unit=y device_type=y appinfo=y
[cx-unlock] web-article: source_id=102489045 attr=5 请求鉴权…
[cx-unlock] web article unlocked, han=1474
判断小技巧:一次调用如果在 1 毫秒内就 Script Completed,说明它没发出任何网络请求,是同步提前返回;正常解锁至少会有一条子请求,耗时是几十到几百毫秒。
对照判断:
| 日志表现 | 原因 | 处理 |
|---|---|---|
完全没有 [cx-unlock] 开头的行 |
脚本没被触发 | 检查 MITM 已开启、CA 已信任、模块已启用;模块的 MITM 列表里能看到 *.caixin.com |
arg=空 |
argument= 没传进来 |
模块版本太旧或参数表未生效,把模块删掉重装 |
cred=无 或某字段 =n |
凭据没填 / 填错字段 | 在模块参数界面补齐 uid / code / unit / deviceType |
服务端拒绝 status=-7 |
凭据与账号不符 | unit / deviceType 必须与 Cookie 里的 SA_USER_UNIT / SA_USER_DEVICE_TYPE 完全一致 |
Referer 无法解析 + 改用近期 source_id |
跨域请求的 Referer 只带 origin | 正常兜底:脚本会改用 App 打开文章时带明文 id 的请求(getchargearticle / savev2Action / purchase/article/get)记下的 id |
既无 Referer 也无近期 source_id |
打开文章后没有出现过带明文 id 的请求 | 先随便打开一次该文章(让它走一次网络),或告诉我,我再补别的 id 来源 |
鉴权请求失败 |
签名或网络问题 | 确认 uid 是纯数字;看 Surge 日志里这条子请求的状态码 |
attr=0 非付费 |
该文章本来免费 | 正常,无需解锁 |
另外:凭据缺失或服务端拒绝时,Surge 会弹一条通知(同类 10 分钟最多一次),比翻日志更快。
临时用不了参数表时,可以把 argument= 整段换成旧写法(脚本同时兼容):
argument=cookie=SA_USER_UID=…; USER_LOGIN_CODE=…; SA_USER_UNIT=1; SA_USER_DEVICE_TYPE=4; appinfo=…
实现要点
- 重放请求带
X-CX-Unlock: 1头,脚本见到即放行,避免自我递归。 - App 链路必须重放服务端响应,不能注入网页版的干净正文。 App 端会对
content执行一段尾部混淆清理:干净正文会被当成混淆文本、删掉每 50 个汉字位置的字符。只有服务端自己加过标记的内容才能被正确还原。 - 不做分页拼接。 实测
page=1/2返回的是同一份全文且totalPage变成 0,按totalPage拼页只会把正文拼两遍。恒传page=0。 - 页面 JS 不会覆盖注入结果:
pagebar_v7_test_v2.js只在_JsonIsOK且status > 0时才$('#Main_Content_Val').html(content);匿名时服务端返回status: -8,前端还会强制改成-1000。#chargeWall会被.show()打开,靠注入的!importantCSS 压掉。 newValidate不能依赖 Referer:它是跨域请求(mappv5→gateway),WKWebView/Safari 默认strict-origin-when-cross-origin只发 origin(https://mappv5.caixin.com/),路径被砍掉,拿不到 appId。脚本改为从 App 其它带明文 id 的请求里记"当前文章"来兜底。appinfoCookie 可以直接当appinfo请求头用(同一把 DES key1234abcd加密,服务端不校验字段顺序),因此脚本不需要实现 DES。
已知边界
- 服务端判权逻辑没有变。模块给的是该账号本来就能看的内容;账号过期或没有对应权益时,一样什么都拿不到。
- 需要开启 MITM;不消费赠阅次数与单次阅读卡,不写账号状态。
- 网页接口的
content是干净的,App 接口的content带尾部混淆标记,两者不要混用。 - 接口随时可能变;本文档与脚本基于 2026-09-28 的线上行为。
文件
| 文件 | 说明 |
|---|---|
caixin-unlock.sgmodule |
Surge 模块,一条脚本行,argument= 里配置 Cookie |
caixin-unlock.js |
单文件脚本,含内置的网页签名私钥与 App RSA 公钥 |
许可
仅供个人在自己有权访问的账号上使用。请遵守财新网的服务条款。