No description
  • JavaScript 100%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
congshuhao 70a5efe586 App 链路不再依赖 Referer:跨域只带 origin,改用近期明文 id 兜底
定位: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 行为
2026-09-28 13:48:18 +08:00
caixin-unlock.js App 链路不再依赖 Referer:跨域只带 origin,改用近期明文 id 兜底 2026-09-28 13:48:18 +08:00
caixin-unlock.sgmodule App 链路不再依赖 Referer:跨域只带 origin,改用近期明文 id 兜底 2026-09-28 13:48:18 +08:00
README.md App 链路不再依赖 Referer:跨域只带 origin,改用近期明文 id 兜底 2026-09-28 13:48:18 +08:00

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() 打开,靠注入的 !important CSS 压掉。
  • newValidate 不能依赖 Referer:它是跨域请求(mappv5 → gateway),WKWebView/Safari 默认 strict-origin-when-cross-origin 只发 origin(https://mappv5.caixin.com/),路径被砍掉,拿不到 appId。脚本改为从 App 其它带明文 id 的请求里记"当前文章"来兜底。
  • appinfo Cookie 可以直接当 appinfo 请求头用(同一把 DES key 1234abcd 加密,服务端不校验字段顺序),因此脚本不需要实现 DES。

已知边界

  • 服务端判权逻辑没有变。模块给的是该账号本来就能看的内容;账号过期或没有对应权益时,一样什么都拿不到。
  • 需要开启 MITM;不消费赠阅次数与单次阅读卡,不写账号状态。
  • 网页接口的 content 是干净的,App 接口的 content 带尾部混淆标记,两者不要混用。
  • 接口随时可能变;本文档与脚本基于 2026-09-28 的线上行为。

文件

文件 说明
caixin-unlock.sgmodule Surge 模块,一条脚本行,argument= 里配置 Cookie
caixin-unlock.js 单文件脚本,含内置的网页签名私钥与 App RSA 公钥

许可

仅供个人在自己有权访问的账号上使用。请遵守财新网的服务条款。