dsh-bili-whale WBI签名实现解析:从nav密钥到w_rid计算,B站接口请求的完整链路拆解

【免费下载链接】dsh-bili-whale

给 DSH 装一条有日子的鱼:自己按作息刷 B 站、真看懂视频、写两本日记,把值得的 @ 你。

项目地址:https://gitcode.com/gh_mirrors/ds/dsh-bili-whale
点击查看免费下载

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 时间戳

签名前对业务参数做三件事:

  1. 注入wts:Math.floor(now / 1000)的秒级时间戳(B站 用它判断请求时效);
  2. 删掉w_rid:防止上一轮的旧签名参数混进新请求;
  3. 排序 + 编码:所有 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/playurlsignedPaths
⑤节流进程内两次请求至少隔 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.mjsWBI 核心: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 站、真看懂视频、写两本日记,把值得的 @ 你。

项目地址:https://gitcode.com/gh_mirrors/ds/dsh-bili-whale
点击查看免费下载

相关推荐

上一篇:一次粘贴批量导入100个代理:NeAntik如何创建100个Profile且零网络请求、零泄漏
下一篇:MiroShark完整介绍:如何用1美元和10分钟模拟任何事,终极蜂群智能引擎

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考