dsh-bili-whale WBI签名实现解析:从nav密钥到w_rid计算,B站接口请求的完整链路拆解
【免费下载链接】dsh-bili-whale
给 DSH 装一条有日子的鱼:自己按作息刷 B 站、真看懂视频、写两本日记,把值得的 @ 你。
dsh-bili-whale 是一条「有日子的 B 站鱼」插件:自己按作息刷 B 站、看懂视频、写日记,再把值得的内容 @ 给你。它的每一次读取都依赖 B 站 WBI 签名——本文以 tools/lib/bili-api.mjs 里的signWbi与apiGet为主线,完整拆解「nav 密钥 → mixin_key → 参数排序 → w_rid(MD5)」这条签名链路,以及缓存、熔断、节流如何配合成一次稳妥的 B站接口请求。
一、为什么需要 WBI 签名:从一次真实的 412 崩溃说起
故事的起点是一次「看不懂」的报错。项目早期,debrief/fav/comments/subtitles等六个读命令都直接打旧端点/x/web-interface/view。在部分网络环境下,B站 会返回HTTP 412 + 一整段 HTML,程序崩在JSON.parse上:
SyntaxError: Unexpected token '<'报错完全看不出真实原因 ✗。v0.4.3 的修法很干脆:新增统一中间件apiGet(),把旧路径自动换成 WBI 端点并自动签名,10 个读命令全部改走它(背景见 CHANGELOG.md 的 #18)。也就是说:WBI 签名不是「锦上添花」,而是让请求在不同网络环境下都能走通的地基。
二、signWbi 四步算法:从 nav 密钥到 w_rid
核心实现只有 9 行,位于 tools/lib/bili-api.mjs:
export function signWbi(params, images, now = Date.now()) { const raw = keyOf(images?.img_url ?? '') + keyOf(images?.sub_url ?? '') if (raw.length !== 64) throw new Error('WBI 签名密钥缺失,请稍后重试或检查登录状态') const key = MIXIN.map(i => raw[i]).join('').slice(0, 32) const values = { ...params, wts: Math.floor(now / 1000) } delete values.w_rid const query = Object.keys(values).sort().map(k => `${encodeURIComponent(k)}=${encodeURIComponent(String(values[k]).replace(/[!'()*]/g, ''))}`).join('&') return `${query}&w_rid=${crypto.createHash('md5').update(query + key).digest('hex')}` }下面按步骤拆。
第一步:从 nav 接口取两个「图片」密钥
B站 的GET /x/web-interface/nav会返回data.wbi_img,里面是两个看似随机的图片地址img_url和sub_url。真正的密钥藏在文件名里——keyOf就是把 URL 按/切开取最后一段、再按.切开取第一段,得到一个 32 位十六进制串:
const keyOf = u => String(u).split('/').at(-1).split('.')[0]两个密钥拼起来是 64 位。不足 64 位(比如未登录、nav 异常)会直接抛出「WBI 签名密钥缺失」,而不是带着坏密钥继续发请求 ✗。
⏱ 注意:wbi 密钥会随时间轮换。所以本项目对 nav 单独设了 10 分钟缓存(NAV_TTL_MS),而普通元信息缓存 24 小时——密钥过期是签名失效最常见的原因。
第二步:64 位乱序表生成 32 位 mixin_key
密钥不能直接用,要按一张固定的 64 位索引乱序表重排。这张表写死在 tools/lib/bili-api.mjs:
const MIXIN = [46,47,18,2,53,8,23,32,15,50,10,31,58,3,45,35,27,43,5,49,33,9,42,19,29,28,14,39,12,38,41,13,37,48,7,16,24,55,40,61,26,17,0,1,60,51,30,4,22,25,54,21,56,59,6,63,57,62,11,36,20,34,44,52]算法一句话:key = MIXIN.map(i => raw[i]).join('').slice(0, 32)。即按乱序表从 64 位 raw 里依次取字符,只留前 32 位,得到mixin_key。
第三步:参数清洗、排序,注入 wts 时间戳
签名前对业务参数做三件事:
- 注入
wts:Math.floor(now / 1000)的秒级时间戳(B站 用它判断请求时效); - 删掉
w_rid:防止上一轮的旧签名参数混进新请求; - 排序 + 编码:所有 key 按字典序排序,键值都做
encodeURIComponent,且值里删掉!'()*五个字符——这正是 B站 官方的清洗规则。
第四步:MD5 算出 w_rid
最后一步把拼好的 query 字符串与 mixin_key 拼接,取 MD5:
w_rid = md5(query + mixin_key)最终返回query + '&w_rid=' + w_rid。这个 32 位十六进制串就是 B站 服务端校验「请求参数没被篡改、时间还新鲜」的凭据。
用标准测试向量验证签名
项目在 tests/supplement.test.mjs 里固化了一组已知输入 → 已知输出的向量,任何改动只要签名逻辑对,结果必须一字不差:
signWbi({foo:'114', bar:'514', baz:1919810}, images, 1702204169000) // → 'bar=514&baz=1919810&foo=114&wts=1702204169&w_rid=6149fdadf571698ca7e6a567265cd0ee'其中images是一对 32 位十六进制文件名的 nav 图片。测试还顺手断言了「密钥缺失必须抛错」(tests/supplement.test.mjs)。这是学习 WBI 签名时最可靠的自测锚点。
三、apiGet 完整请求链路:签名只是其中一环
真正发请求的是 apiGet(),签名嵌在一条「先防御、再签名、后风控」的流水线里:
| 顺序 | 环节 | 作用 | 源码位置 |
|---|---|---|---|
| ① | 先算缓存键 | 在改写路径之前算 key:去掉每次都变的wts/w_rid、参数排序、账号哈希作前缀 | cacheKey |
| ② | 熔断检查 | 正处于 412 退避期?能吃 7 天内旧缓存就吃,否则明确告诉你「约 N 分钟后恢复」 | banned 分支 |
| ③ | 缓存命中 | 元信息(view / 用户信息)缓存 24h,nav 只 10h→10min;评论、私信等动态内容绝不缓存 | CACHEABLE |
| ④ | WBI 签名 | 仅两个端点自动换签名端点:view → wbi/view、playurl → wbi/playurl | signedPaths |
| ⑤ | 节流 | 进程内两次请求至少隔 300ms,「贴脸连打」正是被限流的典型特征 | throttle |
| ⑥ | 发送 | 走guardedFetch:跨进程读 3000/天、写 30/天、全局间隔 ≥600ms、防重复发送 | traffic.mjs |
| ⑦ | 风控响应 | HTTP 412/429 或 code-412/-352/-799/-509/-10403→ 指数退避 5→10→20→30 分钟封顶 | 412 处理 |
| ⑧ | 成功落盘 | code=0时清零退避计数、写缓存(最多留 400 条防膨胀) | 写缓存 |
两个容易忽略的设计细节:
- 缓存键必须在改写路径前算。源码注释写得很直白:「读」用的是原始
/x/web-interface/view?bvid=…,「写」如果等 URL 被改成?…&w_rid=…(每次都不同)就永远命中不了 ✗(tools/lib/bili-api.mjs)。 - 熔断期间不装死。撞风控后不再硬闯网络,能吃到过期缓存就吃,吃不到就返回带「还要等多久」的清楚错误,而不是一个裸的
-412。
四、同一套签名在项目里的多处落地
signWbi是统一版,但部分命令行脚本为了独立可跑,各自带了一份等价实现(源码注释原话:「各脚本里重复了一份,保持一致;要改就一起改」):
- 搜索:tools/commands/search.mjs 用同一算法签名后请求
/x/web-interface/wbi/search/type,是这条链路最完整的「手动版」演示; - 看视频复盘:tools/commands/debrief.mjs 与 tools/commands/feed.mjs 各有一份 mixin 计算;
- 读懂视频:tools/commands/video-content.mjs 的
wbiQuery专用于/x/player/wbi/v2——注释点破了一个坑:字幕接口必须带登录 cookie 且走 WBI 签名,否则拿到的subtitle_url缺auth_key,CDN 直接 403; - 直播弹幕 / 字幕 / 账号诊断:tools/commands/danmaku.mjs、tools/commands/subtitles.mjs、tools/diagnostics/whois.mjs 同样先取 nav 再算 mixin。
读代码时建议先读统一的signWbi,再拿search.mjs当「展开版」对照,最后回到apiGet看它如何被包装成带防御层的正式链路。
五、关键文件清单
| 文件 | 内容 |
|---|---|
| tools/lib/bili-api.mjs | WBI 核心:signWbi、apiGet、缓存与熔断 |
| tools/lib/traffic.mjs | 跨进程流量预算、写操作防重复 |
| tests/supplement.test.mjs | 标准签名向量 + WBI 端点改写测试 |
| tools/commands/search.mjs | 独立签名实现示例 |
| tools/commands/video-content.mjs | 字幕接口的 WBI 用法 |
| CHANGELOG.md | #18 的 412 崩溃背景与修法 |
小结:WBI 签名本身只有「取密钥 → 乱序重排 → 排序清洗 → MD5」四步,真正让它好用的,是apiGet把它嵌进「先缓存、再签名、撞风控就退避」的请求链路里——这正是 dsh-bili-whale 能长期、低频、克制地和 B站 接口相处的底层原因 🐋
【免费下载链接】dsh-bili-whale
给 DSH 装一条有日子的鱼:自己按作息刷 B 站、真看懂视频、写两本日记,把值得的 @ 你。
相关推荐
一张照片完成 AI 换脸:Deep-Live-Cam 实时人脸替换 5 分钟上手指南
一张照片完成 AI 换脸:Deep Live Cam 实时人脸替换 5 分钟上手指南 Deep Live Cam 是一款基于 Python 的开源实时人脸替换工
人工智能AI 应用计算机视觉媒体生成深度学习桌面应用如何破解B站Wbi签名算法:BiliDownloader wbisign实现原理全解析
如何破解B站Wbi签名算法:BiliDownloader wbisign实现原理全解析 BiliDownloader 是一款简洁易用的 B站视频下载器(GUI
Fleet TPM 硬件背书 HTTP 请求签名机制详解:从 TPM 2.0 密钥生成到 SCEP 证书签发的完整链路
Fleet TPM 硬件背书 HTTP 请求签名机制详解:从 TPM 2.0 密钥生成到 SCEP 证书签发的完整链路 fleetd 是 Fleet 的端侧代理
后端前端企业应用应用安全运维GitOps
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考