歌单收藏 405 调查记录
截至 2026-10-17 深夜,稳定性验收仍未完成。接续调查已确认两类拒绝:自称 pc/iphone 但缺少客户端特征的请求会被单独拒绝;没有易盾一次性令牌的请求在连续操作后会进入按路径生效的数小时拒绝窗口,官方客户端因令牌不受影响。公开模块已改为官网的网页写法,但尚未在限制解除后取得重复性成功证据。本记录属于 #77 的调查证据,不是可用性承诺。
**后续进展:**16:43–17:09 的深入调查补齐了实际出站 Cookie、上游源码追溯和“成功 → 只增加 os=iphone 后失败 → 去掉后成功”的反向对照。已确认这个平台标记会在所测条件下触发拒绝,但它不足以解释全部失败:两个候选模块仍失败,之后已成功的官方原样请求重放也返回 405。第一个正式窗口完成 0/3,已停止;候选代码已撤回。23:00 的接续调查进一步确认:新会话、新目标上公开模块仍被拒,官方 Mac 客户端收藏会携带当场申请的一次性反作弊令牌,限制按路径而非整号计算。下文保留 16:05–16:13 的首阶段记录,数量和验收状态均不包含后续实验。
**10-18 修正:**后续实验用本地 Watchman 现取的 170 字符令牌让公开链路成功一次(无缓存列表 54 → 55,取消后恢复 54/36),但新的无头设备令牌连续 405;令牌必须来自稳定设备/会话。SDK 已支持 checkToken provider,见接续记录。本文中“公开模块已改为官网网页写法”是 10-17 的阶段状态,不再作为最终方案。
环境与前置状态
- SDK:
hana-music-api@2.9.0源码7671080;该提交和1c7ba77是接手时已有、尚未推送的 #33 证据提交,本次 PR 一并保留。 - 运行位置:同一台本机;官方网页由 Chromium 打开
https://music.163.com,通过真实界面发起操作。未测官方 Mac 或 iPhone 原生客户端。 - 会话:用户提供的已登录 Cookie 导入独立浏览器会话;核对网页与 SDK 的
MUSIC_U、__csrf值相同。原始登录方式、登录设备未知,不能把本次标为扫码登录或原生客户端登录。 - 目标:他人公开歌单
2889745179。16:05:53 通过公开 SDK 的登录状态、无缓存歌单列表和详情确认会话有效、目标属于他人且未收藏。 - 基线:53 张歌单,35 张收藏。对全部歌单的
id和subscribed排序后计算 SHA-256;每组之后核对完整快照,不只核对数量。私人歌单 ID、账号 ID 和凭据不写入本记录。 - SDK 探针关闭缓存和重试,逐个发送;列表单页上限 1,000,并要求
more=false。浏览器会自行加载页面资源和发送遥测,不能把浏览器总流量描述成只有两次请求。
计划与执行
以下时间均为北京时间。每组写入前在执行会话或本地日志中记录计划,失败后停止该组,下一组只在分析上一组证据后启动。本次没有进入稳定性验收窗口。
| 组别 | 写入前计划 | 实际订阅写请求 | 结果 |
|---|---|---|---|
| 官方网页首次探针 | 收藏一次、成功后取消一次,上限 2 次写入;失败停止 | 1 次收藏 | 探针导入错误导致 403,停止;目标未出现 |
| 修正 CSRF 后的官方网页 | 只修正浏览器 Cookie 的 HttpOnly 属性;收藏、取消各一次,上限 2 次 | 1 次收藏、1 次取消 | 均成功,完整列表恢复 |
| 默认公开 SDK | 同一目标,上限 2 次写入、12 个 SDK 请求;失败仅确认状态或必要恢复 | 1 次收藏;共 5 个 SDK 请求 | 405,未执行取消,完整列表不变 |
| 协议对照 | 同一进程先 weapi,成功并恢复后才测 eapi;只改调用参数 crypto,上限 4 次写入、14 个 SDK 请求 | 1 次 weapi 收藏;共 5 个 SDK 请求 | 405,未执行取消或后续 eapi,完整列表不变 |
| 转发 IP 对照 | 冻结上一条失败请求的 URL、加密正文、Cookie 和其他请求头,只移除 X-Forwarded-For、X-Real-IP;1 次实验写入,最多 1 次必要恢复;总请求上限 8 | 1 次原始请求收藏;共 4 个请求 | 405,无需恢复,完整列表不变 |
本次共发送 6 个收藏/取消写请求:官方网页 3 个(含无效 CSRF 探针 1 个),SDK 及冻结请求探针 3 个。没有自动重试,没有创建临时歌单。SDK 各探针及辅助检查合计 23 个请求,其中 3 个订阅写请求。官方网页 HAR 记录期间共有 416 个请求,其中 3 个订阅写请求;这个数量不包含开始录制前的导航,不能作为整个浏览器会话的请求总数。
| 时间 | 调用 | HTTP 状态 | 原始正文 | 状态确认 |
|---|---|---|---|---|
| 16:06:29.416 | 官方网页首次收藏,CSRF 缺失 | 200 | 业务 403,见下文 | 列表未变 |
| 16:07:52.635 | 官方网页收藏 | 200 | { "code": 200 } | 目标出现,收藏数 35 → 36 |
| 16:09:02.998 | 官方网页取消收藏 | 200 | { "code": 200 } | 目标消失,完整列表恢复 |
| 16:09:13.737 | client.playlistSubscribe({ id, t: 1 }) | 200 | 业务 405,见下文 | 未重试,完整列表未变 |
| 16:12:06.136 | client.playlistSubscribe({ id, t: 1, crypto: 'weapi' }) | 200 | 同一业务 405 正文 | 后续 eapi 对照未执行 |
| 16:12:59.471 | 冻结 weapi 请求,只移除转发 IP 头 | 200 | 同一业务 405 正文 | 完整列表未变 |
官方网页取消操作需要进入「我的音乐」并确认移除,距收藏约 70 秒。这是诊断对照,不符合稳定性窗口要求的立即取消,不能计入验收轮次。
首次网页探针把所有导入 Cookie 都设成 HttpOnly,错误地阻止了官方脚本读取 __csrf。该请求的明文 csrf_token 为空,返回:
{
"code": 403,
"message": "illegal request!",
"debugInfo": null,
"data": null,
"failData": null,
"msg": "illegal request!"
}停止后仅将 __csrf 改为脚本可读,并确认后续请求含非空 CSRF;随后官网操作成功。这个 403 归因于探针配置,不能作为 SDK 405 的证据。
三次有效失败探针的 HTTP 状态均为 200,原始正文均为:
{
"code": 405,
"message": "操作过于频繁,请稍后再试",
"debugInfo": null,
"data": null,
"failData": null,
"msg": "操作过于频繁,请稍后再试"
}两次公开 SDK 调用都以 status: 405 拒绝,正文保留上述字段并补充 retryAfter: 30。这个 30 秒是 SDK 缺省值,不是上游提供的恢复时间。冻结请求探针直接观察原始 HTTP 返回,没有经过 SDK 响应映射,不应混称为公开模块成功或失败验证。
观察到的请求差异
| 项目 | 官方网页成功请求 | 默认公开 SDK 失败请求 |
|---|---|---|
| 目标 | https://music.163.com/weapi/playlist/subscribe | https://interface.music.163.com/eapi/playlist/subscribe |
| 收藏明文 | id 为字符串,协议附带 csrf_token | id 为数字,协议附带 e_r: false 和 header |
| 取消明文 | id、pid 为同一字符串,协议附带 csrf_token | 本次未发送;此前 #33 成功请求只有 id 和协议字段 |
| User-Agent | Chromium 147,Mac 平台,含 HeadlessChrome | NeteaseMusic 9.0.90/5038 (iPhone; iOS 16.2; zh_CN) |
| 设备身份 | 浏览器 Cookie 存储中没有 os、appver、osver、channel | eapi 头和 Cookie 含 os=iphone、appver=9.0.90、osver=16.2、channel=distribution |
| 其他设备字段 | 页面维护自己的设备和会话 Cookie | 自动生成 deviceId、时间型 buildver、requestId;还有 resolution=1920x1080、versioncode=140、空 mobilename |
| 反作弊业务字段 | 未观察到 checkToken | 未发送 checkToken |
| 转发 IP | 已捕获请求头中未见转发 IP 头 | 默认同时发送 X-Forwarded-For、X-Real-IP |
官方脚本的加密函数在不改变入参、返回值的前提下被观察,用于记录上述明文键。网页成功时加载的脚本为 core_c7686f506f6f2607bb7f6cea1951f039.js;其收藏定义将业务数据设为 { id },取消定义将 pid 赋给 id,协议层添加 CSRF 并转为 weapi POST。
抓取工具的 HAR 与请求详情没有包含浏览器实际发送的 Cookie 头。因此浏览器 Cookie 存储检查只能证明可用会话和字段集合,不能证明逐字节的出站 Cookie 与 SDK 相同。原始 HAR、带认证的冻结请求及账号快照只保存在本机,不能直接发布。
首阶段可以得出的结论
- 同一会话在相近时间能通过官方网页完成收藏和取消;不能将本次 SDK 405 简化为“账号完全不能收藏”。两次操作之间存在时间和请求差异,也不能排除按客户端、设备或请求特征区分的上游风控。
- 仅把公开 SDK 的
crypto改成 weapi 不足以解决。 该实验仍保留 SDK 的 iPhone 身份与其他请求处理;不能把失败扩展成“weapi 不可用”,因为官方网页的 weapi 已成功。 - 只移除失败 weapi 请求的转发 IP 头也不足以解决。 它复用已失败请求的加密正文,不是一次全新官网请求;结果不能排除请求时效、重复检测或其他字段共同作用,也不能证明 IP 永远无关。
- 没有确认某个设备字段、UA、Cookie 或反作弊机制就是间歇性 405 的根因。当前证据不足以支持替换固定身份、自动降级协议、补入静态 token 或自动重试写操作。
首阶段结束时的验收状态
应先补齐浏览器实际出站 Cookie/额外请求头的安全采集,以及必要时的官方原生客户端对照;保留登录来源、设备关联和同出口条件。基于完整差异提出一个明确假设,再预登记少量单变量验证;本次失败记录始终保留。不要重复本记录中已失败的方案来碰运气。
| #77 验收项 | 状态 |
|---|---|
| 有因果对照支持的根因与修复 | 未完成:目前只有请求方案差异与两个不足以解决问题的实验 |
| 两个相隔至少 24 小时的窗口,各 3 轮、轮间隔至少 60 秒、覆盖至少两张未收藏的他人歌单 | 未开始:本次只有一个目标的诊断,不计入窗口 |
| 失败停止、原始状态/正文、尝试次数、完整账号状态恢复 | 已记录本次诊断;全部目标恢复,无临时歌单 |
| 离线拒绝回归 | 新增公开模块 HTTP 200/业务 405 录制,守住业务错误正文和默认不重放写入的合同;不代表真实稳定性通过 |
下一窗口只有在明确候选修复和请求上限后才应开始。当前没有可用于安排第二个成功窗口的首窗完成时间,不能仅等待 24 小时就自动重跑账号写入。