B站WBI签名逆向解析:从JS混淆到Python复现的完整指南
1. 项目概述为什么B站的WBI签名值得深究如果你尝试过用脚本或者自己写程序去抓取B站的一些数据比如视频信息、评论列表或者用户动态大概率会在某个请求的URL里看到一串长长的、以w_rid开头的参数。这串字符就是B站目前主流的Web端接口签名——WBI签名。它像一道门禁拦住了无数简单粗暴的requests.get。今天我们不谈那些只能抓抓静态页面的皮毛而是直接深入到这道门禁的核心把生成w_rid的整个JavaScript逻辑从混淆的代码里一点点扒出来直到我们能在Python里完美复现。这不仅仅是一个“逆向”练习。理解WBI签名意味着你真正理解了B站前端与后端交互的鉴权逻辑。这对于开发B站相关的数据工具、自动化脚本甚至是研究其API设计思路都有着至关重要的作用。你会发现它背后是一套融合了时间戳、随机数、参数排序和特定算法比如MD5的完整方案。网上虽然有一些现成的库或代码片段但要么已经过时要么只知其然不知其所以然。我们这次的目标是让你不仅能“用”更能“懂”能从零开始亲手拆解并重建这个签名过程。2. WBI签名核心原理与流程拆解在开始逆向之前我们必须先搞清楚WBI签名要解决什么问题以及它的标准流程是什么样的。这能帮助我们在面对一堆混淆的JS代码时知道该寻找什么。2.1 签名的作用与位置B站的大部分API接口尤其是那些需要验证用户身份或涉及敏感操作的接口都会要求携带WBI签名。这个签名的主要作用是防篡改确保客户端发送给服务器的请求参数在传输过程中没有被修改。防重放通过引入时间戳wts和随机数防止同一个请求被重复发送攻击。身份辅助验证虽然核心身份凭证是SESSDATA等Cookie但WBI签名作为一层额外的安全加固增加了自动化脚本直接调用API的难度。签名结果w_rid通常作为查询字符串Query String附加在请求的URL末尾例如https://api.bilibili.com/x/web-interface/wbi/search/all/v2?keywordtestwts1691234567w_ridabcdef12345678902.2 标准生成流程理论模型通过对B站前端代码的长期观察和逆向经验我们可以总结出WBI签名的大致流程。注意这是逆向分析后得出的“黑盒”模型具体实现细节需要从代码中验证。参数收集与排序将所有待签名的参数包括wts时间戳按照字典序ASCII码进行升序排列并拼接成key1value1key2value2...格式的字符串。这里有个关键wts参数本身也是参与签名计算的一部分。获取混合密钥B站前端会从某个接口通常是/x/web-interface/nav或内联的JavaScript变量中获取两个关键的字符串我们称之为sub_key。这两个密钥是动态的会不定期更换。密钥混合与生成将两个sub_key以某种固定顺序和规则进行拼接或混合生成一个最终的“盐值”salt或称为“混合密钥”。拼接与哈希计算将第1步生成的参数字符串与第3步生成的混合密钥拼接在一起形成一个完整的待计算字符串。对这个字符串进行MD5哈希计算得到32位的十六进制字符串这就是最终的w_rid。用公式简单表示就是w_rid md5( ordered_param_string mixed_secret_key )我们的逆向工作核心就是要找到步骤2中sub_key的获取来源和方式以及步骤3中混合密钥的具体生成算法。这些逻辑都被高度混淆和隐藏在B站的主JavaScript文件如security.min.js或主入口js中。3. 逆向工程实战定位与解析关键代码理论清晰后我们进入实战环节。逆向JS没有银弹主要依靠浏览器开发者工具和一定的耐心。3.1 环境准备与初步抓包首先你需要一个浏览器推荐使用Chrome或基于Chromium的Edge。打开B站首页 (www.bilibili.com) 并登录你的账号。按F12打开开发者工具切换到Network网络面板。勾选Preserve log保留日志防止页面跳转时请求记录被清空。刷新页面在纷繁复杂的网络请求中寻找一个名为nav的请求。它的接口地址通常是https://api.bilibili.com/x/web-interface/nav。这个请求的响应体里就藏着我们需要的第一个线索——wbi_img。在nav接口的JSON响应里你会看到类似这样的结构{ code: 0, data: { wbi_img: { img_url: https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png, sub_url: https://i0.hdslb.com/bfs/wbi/4932caff0ff746eab6f01bf08b70ac45.png } } }这里的img_url和sub_url的URL中文件名部分如7cd084941338484aae1ad9425b84077c和4932caff0ff746eab6f01bf08b70ac45就是那两个原始的sub_key。但请注意它们并不是直接用于签名的密钥。B站前端会从这里提取出字符串再经过一步处理。3.2 关键JavaScript逻辑定位接下来是最关键的一步找到处理这些密钥和生成签名的JavaScript代码。在开发者工具的Sources源代码面板中使用CtrlShiftF进行全局搜索。尝试搜索关键词如wbi、w_rid、mix、img_key、sub_key。由于代码被混淆直接搜索可能找不到。更有效的方法是搜索一些常量字符串比如wbi可能被混淆成\u0077\u0062\u0069Unicode转义但现代混淆工具可能会改变形式。更可靠的方法——断点调试回到Network面板找到一个携带了w_rid的请求比如搜索接口。在这个请求上右键选择Copy - Copy as cURL。虽然我们现在不用cURL但可以观察这个请求的URL记住它的参数。在Sources面板按CtrlP快速打开文件输入(index)或main找到主页面加载的JS文件通常是一个很大的、带哈希值的文件如security.xxxxxx.js。在这个JS文件里按CtrlF搜索w_rid。由于混淆它可能被拆散。尝试搜索rid或wts。找到类似params[w_rid] ...或e.push(w_rid r)这样的代码行在它前面一行打上断点点击行号。回到网页触发一个会发送带签名请求的动作比如进行搜索。代码执行会在你的断点处暂停。注意B站的JS混淆和打包策略经常变化上述搜索关键词和文件位置可能失效。核心思路是通过带签名的网络请求反向定位到生成该签名的JS代码执行处。3.3 核心算法拆解从sub_key到mix_key当你的断点生效代码执行暂停后你就可以开始观察调用栈Call Stack和局部变量Scope Local了。这是逆向的黄金时刻。你需要一步步“步过”F10或“步入”F11跟踪执行流程直到找到那个将img_key和sub_key混合并最终进行MD5计算的函数。根据对历史版本和当前请注意算法可能更新代码的分析混合算法通常如下从nav接口返回的img_url和sub_url中提取文件名部分即sub_key。假设我们得到img_key_str 7cd084941338484aae1ad9425b84077csub_key_str 4932caff0ff746eab6f01bf08b70ac45这两个字符串会经过一个固定的、硬编码的“混淆表”进行重组。这个表是一个包含46个字符的字符串类似于一个自定义的映射规则。在过去的版本中这个表可能是ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/的变体或子集。算法会遍历这两个sub_key根据它们在“混淆表”中的位置按照某种规则交叉选取字符最终生成一个固定长度通常是32位的新字符串这就是最终的mix_key或secret_key。一个简化后的Python示例用于说明混合逻辑并非真实算法真实算法需逆向得出def mix_keys(img_key, sub_key): # 假设的混淆表 (真实情况需要从JS中提取) mix_table abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/ mixed_chars [] # 假设的混合规则取两个key对应位置字符在表中的索引相加后取模再用新索引从表中取字符 for i in range(min(len(img_key), len(sub_key))): idx1 mix_table.find(img_key[i]) % len(mix_table) idx2 mix_table.find(sub_key[i]) % len(mix_table) new_idx (idx1 idx2) % len(mix_table) mixed_chars.append(mix_table[new_idx]) return .join(mixed_chars)[:32] # 截取前32位重点你必须从当前有效的JS代码中还原出真实的mix_table和混合规则。这个规则是静态的、硬编码的一旦找到除非B站整体更新签名方案否则在较长时间内都有效。3.4 参数排序与最终MD5计算得到mix_key后剩下的步骤就相对标准了。参数准备将所有需要发送的请求参数包括wts放入一个字典。wts是当前的Unix时间戳秒级。字典序排序将字典的键值对按照键的ASCII码升序排列。URL编码与拼接将排序后的键值对进行URL编码通常JS的encodeURIComponent会对更多字符编码而Python的urllib.parse.quote默认行为略有不同需要注意然后用和拼接成字符串。例如keywordtestwts1691234567。实操心得这里有个巨坑。JavaScript的encodeURIComponent会将空格编码为%20而Python标准库的urllib.parse.quote默认也会将空格编码为%20看似一致。但对于一些特殊字符如~encodeURIComponent会编码而quote默认不编码。为了完全一致在Python中应使用urllib.parse.quote(string, safe)其中safe表示不对任何非字母数字字符进行保留。更稳妥的方法是直接模仿JS代码中看到的拼接方式有时前端为了性能可能只用encodeURIComponent处理值而键是固定的。拼接密钥与哈希将上一步生成的参数字符串直接与mix_key拼接然后计算MD5。w_rid hashlib.md5((param_str mix_key).encode(utf-8)).hexdigest()。4. Python完整复现与代码实现基于以上的逆向分析我们可以用Python完整实现WBI签名的生成。下面的代码包含了关键步骤的注释并尽量做到了与前端逻辑一致。import hashlib import time import urllib.parse from typing import Dict class BilibiliWBI: B站WBI签名生成器 核心1. 获取并混合img_key和sub_key。 2. 对参数排序并MD5。 # 核心混合密钥表 (此表MUST从当前有效的JS代码中逆向提取此处为示例可能已失效) # 这个表是算法稳定的关键B站更新签名机制往往就是更新这个表或混合规则。 MIX_TABLE ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/ # 核心混合规则函数 (此规则MUST从当前有效的JS代码中逆向提取此处为示例) staticmethod def _mix_keys(img_key: str, sub_key: str) - str: 根据逆向出的JS逻辑混合img_key和sub_key生成最终的secret_key。 这是一个示例函数真实逻辑需要你通过断点调试JS代码来还原。 参数: img_key: 从nav接口wbi_img.img_url文件名提取的字符串 sub_key: 从nav接口wbi_img.sub_url文件名提取的字符串 返回: 混合后的32位密钥 # 示例算法简单拼接后取前32位 (这肯定是错的仅作演示) # 真实情况可能是复杂的字符映射和交叉选取。 mixed img_key sub_key # 更复杂的示例基于MIX_TABLE的索引操作 secret_key [] for i in range(32): # 最终密钥通常是32位 # 假设一个虚构的规则从两个key中交替取字符并用MIX_TABLE转换 char_from_img img_key[i % len(img_key)] char_from_sub sub_key[(i 1) % len(sub_key)] idx1 BilibiliWBI.MIX_TABLE.find(char_from_img) idx2 BilibiliWBI.MIX_TABLE.find(char_from_sub) if idx1 -1 or idx2 -1: # 如果字符不在表中可能直接使用原字符或跳过需根据JS逻辑调整 new_char char_from_img else: new_idx (idx1 idx2) % len(BilibiliWBI.MIX_TABLE) new_char BilibiliWBI.MIX_TABLE[new_idx] secret_key.append(new_char) return .join(secret_key)[:32] def __init__(self, img_key: str, sub_key: str): 初始化传入从nav接口获取的两个原始key。 self.secret_key self._mix_keys(img_key, sub_key) def get_wrid(self, params: Dict[str, str]) - str: 生成w_rid签名。 参数: params: 请求参数字典务必包含wts当前时间戳。 返回: 计算得到的w_rid字符串。 # 1. 参数按ASCII码升序排序 sorted_params sorted(params.items(), keylambda x: x[0]) # 2. 拼接成 key1value1key2value2 格式 # 注意值需要做URL编码编码规则需与JS的encodeURIComponent一致。 param_str .join([ f{k}{urllib.parse.quote(v, safe)} # safe 确保完全编码 for k, v in sorted_params ]) # 3. 拼接密钥并计算MD5 sign_str param_str self.secret_key w_rid hashlib.md5(sign_str.encode(utf-8)).hexdigest() return w_rid def sign_params(self, params: Dict) - Dict: 完整的签名方法添加wts计算w_rid返回包含所有签名参数的新字典。 参数: params: 原始的请求参数字典。 返回: 添加了wts和w_rid的新参数字典。 # 添加时间戳 params_with_ts params.copy() wts int(time.time()) params_with_ts[wts] str(wts) # 计算签名 w_rid self.get_wrid(params_with_ts) # 返回最终参数 params_with_ts[w_rid] w_rid return params_with_ts # 使用示例 if __name__ __main__: # 假设从 https://api.bilibili.com/x/web-interface/nav 获取到 img_key_from_nav 7cd084941338484aae1ad9425b84077c sub_key_from_nav 4932caff0ff746eab6f01bf08b70ac45 # 初始化签名器 wbi_signer BilibiliWBI(img_key_from_nav, sub_key_from_nav) # 构造请求参数 original_params { keyword: JS逆向, search_type: video, order: click, } # 进行签名 signed_params wbi_signer.sign_params(original_params) print(签名后的参数, signed_params) # 输出类似{keyword: JS%E9%80%86%E5%90%91, search_type: video, order: click, wts: 1691234567, w_rid: a1b2c3d4e5f678901234567890123456}代码核心要点说明MIX_TABLE和_mix_keys函数是这个类的灵魂也是逆向工程的主要目标。你必须用浏览器开发者工具从真实的B站JS代码中提取出准确的表和算法替换掉示例中的虚构内容。urllib.parse.quote(v, safe)是为了模拟JavaScriptencodeURIComponent的严格编码行为。这是保证签名一致性的关键细节之一。wts使用秒级时间戳。有些接口可能对时间戳的时效性有要求如几分钟内有效需要注意。5. 常见问题、调试技巧与避坑指南即使你按照上面的流程操作在实际逆向和复现过程中也一定会遇到各种问题。这里分享一些我踩过的坑和调试技巧。5.1 逆向过程常见问题问题1找不到生成w_rid的JS代码在哪里排查B站可能将签名逻辑封装在多个文件或Web Worker中。除了在主JS文件搜索还可以尝试在Network面板查看携带w_rid的请求的Initiator发起者标签页它能显示调用栈直接定位到发起请求的JS代码行。搜索md5或hex_md5等常见MD5函数名可能被混淆尝试搜function (e)内部有rotateLeft等MD5特征运算。在Console面板直接输入window并查看全局变量有时关键函数会挂在window对象下。问题2断点打了但代码混淆严重完全看不懂技巧不要试图理解所有代码。我们的目标很明确找到img_key,sub_key, 混合过程以及最终的MD5调用。关注字符串即使变量名被混淆字符串常量如URL部分、w_rid这个属性名通常只是简单转义相对容易识别。单步执行在疑似区域打上断点后耐心地按F11步入跟进函数内部。观察每一步执行后局部变量Scope的变化特别是那些由长字符串如从nav数据来的key经过一些循环、条件判断后生成的新字符串。Console调试当执行停在某一行时你可以在Console里直接计算当前作用域下的表达式。例如看到var c a b;你可以在Console输入a, b, c来查看它们的值帮助理解逻辑。问题3Python生成的w_rid和浏览器里的不一样系统化排查检查输入是否一致确保你的img_key和sub_key与浏览器当前请求nav接口获得的一模一样。它们可能已经变了。检查混合算法这是最可能出错的地方。在JS代码执行到生成mix_key的地方把img_key,sub_key以及最终的mix_key都打印到Console。然后在你的Python代码里用同样的两个输入key逐步调试你的_mix_keys函数确保每一步生成的中间结果都和浏览器里的一致。检查参数排序与编码在JS签名代码执行前把即将参与签名的参数字典包含wts完整地打印出来。记录下JS中拼接前的参数字符串key1value1key2value2...这个形态。在你的Python代码里用同样的参数字典生成排序拼接后的字符串。逐字符对比这两个字符串。特别注意URL编码的差异比如空格是%20还是波浪号~是否被编码等。检查最终待MD5的字符串在JS计算MD5前一定会有一个字符串param_str mix_key。把这个字符串完整地复制出来。在你的Python代码里在调用hashlib.md5之前也打印出sign_str。这两个字符串必须完全一致包括不可见字符。可以比较它们的长度甚至直接比较它们的MD5值是否相同。5.2 签名失效与更新应对现象之前好用的代码突然所有请求都返回“签名错误”或“-403”。原因B站更新了WBI签名的算法。最常见的是更新了MIX_TABLE或_mix_keys的混合规则。应对重新逆向按照第3部分的流程重新抓包、定位、断点调试提取出新的MIX_TABLE和混合规则。自动化监测进阶对于需要长期稳定运行的服务可以考虑写一个简单的健康检查脚本定期用已知参数测试签名是否有效。一旦失效触发告警提示需要人工介入重新逆向。关注社区像GitHub上的一些热门B站相关项目如爬虫、工具库其Issue和更新日志往往是算法是否变更的“风向标”。5.3 其他实用技巧使用execjs直接调用JS如果逆向出的JS混合算法过于复杂用Python重写很困难或容易出错可以考虑使用PyExecJS或node.js子进程直接执行你从浏览器中提取出来的、经过清理的JS函数。这样能保证100%还原前端逻辑但会引入外部依赖和性能开销。import execjs # 假设你从JS中提取出了混合密钥的函数字符串 js_code function mixKeys(imgKey, subKey) { var mixTable ...; // 你的MIX_TABLE // ... 具体的JS混合算法 return mixedKey; } ctx execjs.compile(js_code) secret_key ctx.call(mixKeys, img_key_from_nav, sub_key_from_nav)注意密钥的缓存img_key和sub_key从nav接口获取这个接口响应可能被缓存或者密钥本身有效期较长。但你的程序不应该假设它永远不变。最佳实践是在程序启动时获取一次并在每次签名失败报403错误时重新获取并更新密钥。尊重平台规则逆向和学习技术是为了理解和解决问题。请务必遵守B站的使用条款不要将获取的API用于恶意爬取、刷量、攻击等违反法律法规和平台规定的用途。控制请求频率避免对对方服务器造成过大压力。逆向工程就像解谜WBI签名是B站设置的一道颇有挑战性的谜题。通过亲手拆解它你不仅能获得一个实用的工具更能深刻理解现代Web应用如何在前端实现复杂的安全逻辑。这个过程锻炼的是你阅读代码、逻辑推理和调试解决问题的能力这些价值远超过一个可用的签名脚本本身。当你最终看到自己Python脚本生成的w_rid能够成功通过B站服务器的验证时那种成就感就是技术人最好的奖励。