跳到主要内容
OSSpark

搜索文章

965 字
5 分钟

浏览器返回 200,CLI 却一直 401:一次 macOS 凭证截断排查

目录

gcoredl-macos 改完受保护音频的授权请求后,第一次拿真实账号试下载,公开章节成功了,三个受保护章节都返回 401。重新复制登录令牌,再跑 CLI 的 auth-check,还是 401。用浏览器发起授权请求,只带同一份来源的令牌,不加额外 Cookie,服务端却返回了 200。

图 001 浏览器授权返回 200 与 CLI 授权返回 401 的对照

图 001 故障现象示意。两边的令牌来自同一处,CLI 发出的值还需要核对。

在此之前,旧版工具直接请求受保护音频接口会得到 403。那一轮改动让 CLI 先请求 JSON 授权结果,再访问其中的短时媒体 URL。这次 401 发生在获取授权的阶段。受保护章节连媒体 URL 都还没拿到,可以先把下载和文件处理放在一边。

图 002 笔记本屏幕上的终端和系统监控面板

图 002 终端工作场景。摄影 Lazar Stankovic,Unsplash License。

用一段测试值检查隐藏输入#

排查先试过令牌解码、Cookie、Referer 和浏览器 User-Agent。这些尝试都没让 CLI 通过。浏览器的 200 提供了一个对照。同一个账号可以完成这次授权,接下来要确认的是 CLI 从哪里读到凭证,以及读到的值有没有变化。

旧登录实现让 /usr/bin/security -w 接收隐藏输入。排查时,用一段独立的 199 字符测试值走同一条输入和保存路径,然后从钥匙串读回,结果只剩 128 字符。这个实验不依赖真实账号,也无需向服务端发请求。即使请求头的格式写对了,用这条路径读到的凭证也已经少了一截。

图 003 独立测试值经旧路径保存前为 199 字符,回读为 128 字符

图 003 历史排查实验的示意,数字只对应当时这条输入和保存路径。

199 和 128 是这次实验的观测值。复查类似问题时,可以用可丢弃的测试值跑通输入、保存和回读,在各阶段记录长度,再在本地比较完整内容。长度不同足以发现截断,长度相同还要检查是否有解码或字符替换。真实令牌无需出现在日志里。

把校验留在登录流程里#

修复后的 CLI 用 Go 的 term.ReadPassword 接收两次隐藏输入,规范化后逐值比较。两次一致才写入钥匙串。写入返回成功后,再读一次并与待保存的值比较,只有一致才算登录成功。

图 004 两次输入一致才写入,写入后回读一致才确认登录成功

图 004 修复后的登录流程。输入不一致或回读校验失败时,登录命令会报错。

凭证还换用了新的钥匙串服务名,避免下载命令继续读到旧条目里已截断的值。成功写入并验证新条目之后,工具会尝试清理旧条目。

两次修复各自验证到了哪里#

修复隐藏输入后,原先返回 401 的三个受保护章节都产生了最终 MP3,连同公开章节一共四个。这次实际下载发生在后续钥匙串 API 迁移之前。

v2.1.1 又把钥匙串读写切到 SecItem* 接口,保留了输入和回读校验。该版本有自动测试和构建记录,真实账号的重新登录、授权和整张专辑下载仍待复测。针对这次变更,复测从清除旧凭证、重新登录开始,先用 auth-check 验证一章的授权,再看整张专辑的下载汇总。

项目代码和使用说明见 gcoredl-macos。