1. 项目概述当爬虫遇上动态加密参数做数据采集的朋友尤其是爬虫工程师对“企查查”这个名字一定不陌生。作为国内领先的企业信息查询平台它不仅是商业调查的利器也常常是我们获取结构化企业数据的“数据源”。然而当你像往常一样打开浏览器开发者工具准备抓取一个企业详情页的接口时可能会发现事情没那么简单。一个看似普通的GET或POST请求其请求头里却多了一个神秘的字段比如X-Hmac-Signature它的值是一长串看似随机、每次请求都在变化的字符。这就是我们今天要面对的核心挑战动态 HMAC-SHA512 参数。这个参数不是摆设它是企查查后端服务器验证请求合法性的“门禁卡”。服务器会用同样的算法和密钥对请求的特定部分如 URL、时间戳、请求体等进行计算得到一个签名值然后与你请求头里带来的签名进行比对。如果一致说明请求是“自己人”发的放行如果不一致或者干脆没有那对不起直接返回 403 或 401 错误你的爬虫程序瞬间“哑火”。这种机制我们通常称之为“请求签名”或“API 签名认证”是反爬虫体系中非常有效且常见的一环它从请求的“身份”层面进行拦截比单纯检查User-Agent、Cookie要高级得多。面对这种情况常规的请求头复制大法从浏览器直接复制cURL命令完全失效因为那个签名是动态的、一次性的。我们必须深入前端 JavaScript 代码找到生成这个签名的算法逻辑、密钥以及参与计算的原始数据我们称之为“签名原文”然后用 Python 或其他语言将其复现出来。这个过程就是JS 逆向。本次实战的目标非常明确逆向分析企查查 Web 端或 App 端通常指其 H5 页面或小程序网络请求中用于签名认证的动态 HMAC-SHA512 参数的生成逻辑并成功用 Python 代码模拟生成从而让我们的爬虫程序能够持续、稳定地发起被后端认可的合法请求。这不仅仅是一次技术破解更是一次完整的学习路径你需要理解 HMAC 算法的原理需要熟练使用浏览器开发者工具进行断点调试需要具备一定的 JavaScript 代码阅读和逻辑分析能力最后还需要将 JS 代码精准地翻译成 Python 代码。整个过程是对爬虫工程师综合能力的一次绝佳锻炼。2. 核心思路与逆向策略拆解在开始动手之前我们不能像无头苍蝇一样乱撞。一个清晰的逆向策略能事半功倍。企查查这类商业站点的反爬策略通常不是单一的签名算法可能嵌套在复杂的代码混淆和流程中。我们的核心思路可以概括为“定位 - 分析 - 还原 - 复现”四步走。2.1 逆向目标与核心问题定义首先我们必须明确逆向的具体目标。一个动态签名参数通常包含以下几个关键要素算法类型确认是 HMAC-SHA512而不是 MD5、SHA256 或其他变种。密钥Secret Key这是签名的核心机密可能硬编码在 JS 中也可能通过更复杂的方式动态获取。签名原文Message即被签名的原始字符串。它由哪些部分按照什么顺序和格式拼接而成常见部分包括HTTP 方法GET/POST。请求的 API 路径如/api/company/detail。排序后的查询参数Query String。排序后的请求体Body如果是 POST 且为x-www-form-urlencoded或json。当前时间戳Timestamp。随机数Nonce。其他固定字符串或版本号。输出格式计算出的哈希值是直接输出还是经过 Base64 或 Hex十六进制编码在请求头里是以什么形式呈现的我们的任务就是在浩如烟海的 JavaScript 代码中找到负责组装“签名原文”和调用 HMAC-SHA512 算法的函数并理清上述所有要素。2.2 工具准备与逆向环境工欲善其事必先利其器。以下是本次实战的必备工具清单浏览器Google Chrome或Microsoft Edge基于 Chromium。它们的开发者工具F12是我们最主要的战场。开发者工具关键面板Network网络记录所有网络请求查看请求头和响应筛选出我们目标 API 的请求。Sources源代码用于查看、搜索和调试 JavaScript 文件。可以设置断点、单步执行是逆向分析的核心。Console控制台可以执行 JavaScript 代码片段用于测试我们找到的函数或变量。浏览器插件EditThisCookie或同类工具方便地查看和编辑 Cookie有时签名会与 Cookie 中的某个session或token关联。ReRes或Local Overrides功能可以映射线上 JS 文件到本地修改后的版本用于持久化我们的调试代码或绕过某些检测但本次实战可能不需要。代码编辑与调试工具VS Code或PyCharm用于编写和调试我们的 Python 复现代码。Node.js有时为了验证算法我们可能需要先在 Node.js 环境下运行我们提取的 JS 代码片段确保逻辑正确。辅助分析网站https://tool.lu/或https://www.sojson.com/用于在线格式化、美化被压缩混淆的 JS 代码虽然浏览器 Sources 面板也自带格式化功能点击{}图标但有时在线工具更强大。https://www.base64encode.org/等用于快速验证编解码结果。2.3 通用逆向流程与心法面对混淆的 JS 代码不要慌张。遵循以下流程一步步拆解网络抓包定位特征在浏览器中打开企查查网站进行能触发目标 API 的操作如搜索公司、查看详情。在 Network 面板中仔细查看该请求的Headers。找到那个可疑的动态参数记下它的名字例如x-sign、authorization、signature。同时注意观察请求的Initiator列它有时会指向发起这个请求的 JS 文件这是一个重要的线索入口。全局搜索缩小范围在 Sources 面板中使用CtrlShiftF进行全局搜索。关键词可以是动态参数名如x-sign。算法名如HMAC、SHA512、createHmacNode.js Crypto 模块方法、CryptoJS.HmacSHA512前端常用库。可能的关键常量如secret、key、sign。请求 URL 的一部分。格式化代码设置断点找到包含关键词的 JS 文件后点击左下角的{}按钮进行代码美化格式化。然后在疑似生成签名的函数调用处例如一个sign函数或一个axios.interceptors.request.use拦截器打上断点。动态调试追踪数据流重新触发请求代码会在断点处暂停。此时利用Scope面板查看局部变量和闭包变量利用Call Stack面板查看函数调用栈。重点关注传入函数的参数是什么函数内部如何拼接字符串最终返回的签名是什么。通过单步执行F10、步入F11、步出ShiftF11像侦探一样追踪数据的流向。提取关键逻辑一旦理清了签名原文的拼接规则和加密调用就将相关的 JS 代码片段提取出来。这可能包括一个工具函数、一个对象配置或者几行关键的逻辑。本地验证与翻译将提取的 JS 代码在浏览器 Console 中或 Node.js 环境中运行用已知的一次请求数据从 Network 面板复制作为输入验证是否能输出与请求头中一致的签名。验证成功后开始将其翻译成 Python 代码。Python 的hmac和hashlib库是标准库完全可以实现相同的功能。注意现代网站普遍使用 Webpack 等打包工具代码模块化严重函数和变量名可能被压缩成单个字母。这时搜索算法库的调用如CryptoJS或观察网络请求的发起栈Initiator会更加有效。同时签名可能不在主业务代码里而是在一个通用的“请求工具”或“SDK”文件中。3. 深入原理HMAC-SHA512 与请求签名机制在动手逆向之前我们有必要从原理上理解对手。这不仅有助于分析更能让我们在复现时避免低级错误。3.1 HMAC 算法消息认证码的核心HMACHash-based Message Authentication Code即基于哈希的消息认证码。它不是一种独立的哈希算法而是一种利用现有哈希函数如 MD5, SHA1, SHA256, SHA512来构造“消息认证码”的技术。它的核心思想是在计算哈希之前将消息Message和一个密钥Secret Key混合起来。这样只有拥有相同密钥的双方才能对相同的消息计算出相同的哈希值即认证码。这解决了单纯哈希如 SHA512无法验证消息来源和完整性的问题。HMAC 的计算过程有标准定义大致如下以伪代码表示key 处理后的密钥如果密钥过长则先哈希过短则补零 opad 外部填充常量0x5c 重复多次 ipad 内部填充常量0x36 重复多次 // 计算内部哈希 inner_hash hash((key XOR ipad) message) // 计算最终 HMAC hmac hash((key XOR opad) inner_hash)幸运的是我们几乎不需要自己实现这个过程无论是 JavaScript 的CryptoJS.HmacSHA512(message, key)还是 Python 的hmac.new(key, message, hashlib.sha512).digest()都封装好了这个标准流程。为什么是 SHA512SHA512 是 SHA-2 家族的一员输出长度为 512 位64字节比 SHA256 更长理论上更安全碰撞概率更低。对于高安全要求的商业 API使用 SHA512 是合理的。3.2 请求签名构建防伪“信封”企查查的 API 签名可以看作是把一次 HTTP 请求打包成一个带有防伪 seal密封章的信封。收集信封内容签名原文把这次请求的核心信息收集起来比如“在2023-10-27 14:30:00时间戳我要用 GET 方法访问/api/v4/company/getDetail这个地址查询参数是id123456”。这些信息按固定顺序拼接成一个字符串。这个顺序非常重要服务器和客户端必须严格一致。盖上密封章HMAC计算用只有服务器和合法客户端知道的“密钥”Secret Key作为印泥对上面拼接好的字符串信封内容进行 HMAC-SHA512 计算得到一串二进制哈希值。贴上密封条编码与传输二进制哈希值不方便在 HTTP 文本协议中传输所以通常会进行 Base64 或十六进制Hex编码变成一个字符串。最后把这个字符串放在 HTTP 请求头的一个特定字段如X-Sign里随请求一起发送给服务器。服务器验章服务器收到请求后做完全相同的事情用同样的密钥按照同样的规则拼接签名原文计算 HMAC-SHA512然后比较计算结果和请求头里带来的签名是否一致。一致则通过不一致则拒绝。这种机制的强大之处在于防篡改如果攻击者中途修改了请求的任何部分如参数由于签名原文变了计算出的签名会完全不同服务器会拒绝。防重放签名原文里通常包含时间戳Timestamp和随机数Nonce。服务器可以检查时间戳是否在可接受的时间窗口内如±5分钟并记录近期使用过的 Nonce从而防止同一个请求被重复发送重放攻击。身份验证只有拥有正确密钥的客户端才能生成有效的签名实现了对客户端的认证。我们的逆向就是要破解这个“信封”的封装规则和“印泥”密钥。4. 实战逆向定位并分析企查查签名逻辑理论准备就绪现在让我们进入实战环节。由于企查查的具体代码会随时间更新以下过程是一种通用的、方法论层面的演示你需要根据当时网站的实际情况进行调整。4.1 第一步网络抓包与特征确认打开 Chrome 浏览器进入企查查官网 (www.qichacha.com)。按F12打开开发者工具切换到Network面板。确保勾选了Preserve log保留日志。在网站上执行一个明确的搜索操作例如在搜索框输入一个公司名点击搜索。在 Network 面板中你会看到一系列请求。寻找返回公司列表或详情的 API 请求。通常它们的 URL 会包含/api/、/v4/、/search等关键词响应类型Type是xhr或fetch。点击这个目标请求在右侧的Headers选项卡中向下滚动到Request Headers部分。仔细寻找看起来像加密字符串的字段。常见的嫌疑字段名有X-SignX-SignatureAuthorization(可能包含Bearer以外的自定义方案)X-Hmac-SignatureQC-SignToken(但可能是静态的) 例如你可能会看到X-Sign: V2RGeVFtUXlNVFE0TURrM09UazVNRGt5TlRrM09UazVPVE13TURrM09Uaz0这串字符末尾有强烈暗示它是 Base64 编码的。记下这个字段名和一次具体的值。同时记录下这次请求的完整 URL、方法GET/POST、以及所有的 Query Parameters 和 Request Payload如果有。4.2 第二步全局搜索与初步定位切换到Sources面板。按CtrlShiftF打开全局搜索框。输入我们上一步找到的签名头字段名比如X-Sign进行搜索。注意匹配大小写。如果直接搜索字段名没有结果可能是因为代码被压缩字段名被作为字符串常量放在了别处。可以尝试搜索sign小写或者hmac、sha512。更有效的方法是回到 Network 面板点击目标请求在右侧的Initiator选项卡里查看这个请求是由哪个 JS 文件发起的。点击那个文件名可以直接跳转到 Sources 面板的对应文件。跳转后首先点击左下角的{}按钮Pretty-print来格式化这份可能被压缩成一行的代码。4.3 第三步关键代码分析与断点调试假设我们通过搜索hmac或sha512在一个名为app.xxxxxx.js的文件里找到了疑似代码。格式化后我们可能会看到类似下面的结构这是模拟的、简化后的代码// 模拟代码非真实企查查代码 function generateSign(url, params, data, timestamp) { // 1. 对参数进行排序并拼接 var sortedParams Object.keys(params).sort().map(function(key) { return key encodeURIComponent(params[key]); }).join(); // 2. 如果有请求体也处理假设是JSON var dataStr ; if (data Object.keys(data).length 0) { dataStr JSON.stringify(data); } // 3. 拼接签名原文 var signString [url, sortedParams, dataStr, timestamp].join(|); console.log(Sign String:, signString); // 调试用 // 4. 使用 CryptoJS 计算 HMAC-SHA512 var secretKey aVerySecretKey123456; // 密钥可能在这里也可能从别处获取 var hash CryptoJS.HmacSHA512(signString, secretKey); // 5. 将二进制哈希转换为 Base64 字符串 var signBase64 CryptoJS.enc.Base64.stringify(hash); return signBase64; } // 在请求拦截器中调用 axios.interceptors.request.use(function(config) { var timestamp Date.now().toString(); config.headers[X-Timestamp] timestamp; // 假设我们从 config 中提取必要信息 var sign generateSign(config.url, config.params, config.data, timestamp); config.headers[X-Sign] sign; return config; });分析要点找到入口我们找到了generateSign函数和请求拦截器。这就是签名的生成入口。理解逻辑signString的拼接方式是url|sortedParams|dataStr|timestamp。这是签名原文的格式是逆向的核心成果之一。它使用了CryptoJS.HmacSHA512函数确认了算法。密钥secretKey硬编码在函数里aVerySecretKey123456。在实际中密钥可能不在这里而是通过某个接口获取或者是从一个更大的配置对象中读取甚至是通过更复杂的算法动态生成。这是逆向的另一个关键点。最终输出做了 Base64 编码。设置断点在generateSign函数的第一行和return行打上断点。触发调试回到网页再次触发搜索请求。代码会在断点处暂停。观察数据在Scope面板查看url,params,data,timestamp的值确认它们是否与 Network 面板中看到的请求信息一致。单步执行F10观察sortedParams和signString的生成过程。在 Console 中打印signString复制下来。继续执行得到signBase64。与 Network 面板中请求头里的X-Sign值对比如果一致那么恭喜你核心逻辑找到了实操心得在实际逆向中代码往往被严重混淆变量名可能是a,b,c,t,e等。这时候不要纠结于变量名而要关注数据流和操作序列。比如看到Object.keys(t).sort()就要意识到这是在排序看到.map(...).join()就要意识到这是在拼接查询字符串看到CryptoJS.HmacSHA512或e.createHmac(sha512, n).update(s).digest(base64)这样的调用就要知道这是加密核心。抓住这些关键操作节点就能像拼图一样还原出逻辑。4.4 第四步密钥的寻找密钥是签名的灵魂。如果它像上面例子一样硬编码在代码里那是最简单的情况。但更多时候它会被隐藏全局变量可能是一个在更早初始化的全局变量如window._secretKey或__QC_CONFIG__.key。接口获取可能在页面初始化时通过一个不显眼的 API 请求从服务器获取然后存储在内存或sessionStorage中。你需要在 Network 中寻找更早的、可能返回密钥的请求。本地计算可能由页面的某些固定信息如用户ID、设备指纹通过一个固定算法计算得出。这需要你逆向计算过程。分段存储密钥被拆分成多个部分分散在代码的不同位置使用时再拼接。寻找密钥的技巧在格式化后的 JS 代码中搜索secret、key、appkey、appSecret等关键词。在generateSign函数内部对secretKey变量右键点击 “Find all references”查看它在哪里被定义和赋值。在 Console 中尝试输入可能存在的全局变量名如window.QC_SECRET看是否有返回值。如果密钥来自接口在 Network 中过滤XHR/Fetch请求查看响应体寻找包含长字符串的字段。5. Python 复现从 JS 逻辑到可运行代码一旦我们在浏览器中成功验证了签名逻辑下一步就是用 Python 将其复现集成到我们的爬虫程序中。5.1 环境准备与核心库确保你的 Python 环境安装了必要的库。我们主要使用标准库。# 通常不需要额外安装hmac 和 hashlib 是标准库 # 但为了处理 URL 编码和 JSON我们会用到 urllib 和 json它们也是标准库。5.2 代码复现详解假设我们逆向出的逻辑如下基于之前的模拟代码签名原文格式{url}|{sorted_query_string}|{request_body_json}|{timestamp}请求体data为空时用空字符串表示。查询参数params需要按键名升序排序并按keyvalue用连接value需要做 URL 编码。密钥secret_key为固定字符串aVerySecretKey123456。使用 HMAC-SHA512 算法。输出结果进行 Base64 编码。下面是用 Python 实现的代码import hmac import hashlib import base64 import time import json from urllib.parse import urlencode, quote_plus class QichachaSignGenerator: def __init__(self, secret_keyaVerySecretKey123456): 初始化签名生成器 :param secret_key: 从JS逆向中获取的密钥 self.secret_key secret_key.encode(utf-8) # 密钥需要转换为bytes def generate_signature(self, method, url_path, paramsNone, bodyNone, timestampNone): 生成企查查请求签名 :param method: HTTP方法如 GET, POST :param url_path: API路径如 /api/v4/company/getDetail :param params: 字典查询参数 :param body: 字典请求体JSON格式 :param timestamp: 时间戳毫秒如果为None则使用当前时间 :return: 计算得到的签名字符串 (Base64) if timestamp is None: timestamp int(time.time() * 1000) # 当前时间戳毫秒 timestamp_str str(timestamp) # 1. 处理查询参数 sorted_query_str if params and isinstance(params, dict): # 按键名升序排序 sorted_params sorted(params.items(), keylambda x: x[0]) # 拼接成 k1v1k2v2 格式并对value进行URL编码 encoded_items [] for key, value in sorted_params: # 注意这里使用 quote_plus 对值进行编码模拟JS中的 encodeURIComponent # encodeURIComponent 会将空格转为 %20而 quote_plus 会转为 。 # 企查查后端可能使用其中一种需要根据JS代码确认。 # 通常更安全的做法是使用 quote(value, safe)它对应 encodeURIComponent。 # 这里先假设使用 quote_plus实际需调试确定。 encoded_value quote_plus(str(value)) if value is not None else encoded_items.append(f{key}{encoded_value}) sorted_query_str .join(encoded_items) # 2. 处理请求体 body_str if body and isinstance(body, dict): # 将body字典转换为JSON字符串。注意JS中JSON.stringify会对键进行排序吗 # 默认不会但有些实现会先对body的键排序再stringify。 # 我们需要根据逆向结果确定。假设这里不需要排序直接转换。 body_str json.dumps(body, separators(,, :), ensure_asciiFalse) # 紧凑格式无空格 # 如果逆向发现JS中对body的键也排序了则需要 # body_str json.dumps(body, sort_keysTrue, separators(,, :), ensure_asciiFalse) # 3. 拼接签名原文 # 根据逆向结果格式为 url|sorted_params|body|timestamp # 注意url 是 path 还是 full url 模拟代码中是 config.url可能包含查询参数。 # 但我们的 sorted_query_str 已经单独处理了所以这里的 url 应该是不带查询参数的路径。 # 需要根据JS逻辑确认。假设是 path。 sign_message_parts [ url_path, # API路径 sorted_query_str, # 排序后的查询字符串 body_str, # 请求体JSON字符串 timestamp_str # 时间戳 ] sign_message |.join(sign_message_parts) print(f[DEBUG] 签名原文: {sign_message}) # 调试时打印正式使用可移除 # 4. 计算 HMAC-SHA512 # 注意sign_message 需要转换为 bytes message_bytes sign_message.encode(utf-8) hmac_obj hmac.new(self.secret_key, message_bytes, hashlib.sha512) # 5. 获取二进制摘要并进行Base64编码 digest_bytes hmac_obj.digest() signature_b64 base64.b64encode(digest_bytes).decode(utf-8) return signature_b64, timestamp_str # 使用示例 if __name__ __main__: signer QichachaSignGenerator(secret_keyaVerySecretKey123456) # 替换为真实密钥 # 模拟一次请求 method GET url_path /api/v4/company/getDetail params { companyId: 123456789, pageIndex: 1, pageSize: 20 } body None # GET请求通常无body # body {someKey: someValue} # 如果是POST请求 signature, ts signer.generate_signature(method, url_path, params, body) print(f生成的时间戳: {ts}) print(f生成的签名: {signature}) # 构造请求头 headers { X-Timestamp: ts, X-Sign: signature, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., # ... 其他必要头信息 } print(f请求头示例: {headers})5.3 关键细节与调试技巧URL 编码的坑JavaScript 的encodeURIComponent和 Python 的urllib.parse.quote或quote_plus在处理空格、加号等字符时略有不同。如果签名校验失败首先检查这里。一个稳妥的方法是在 JS 调试时把即将被编码的字符串和编码后的结果打印出来然后在 Python 中模拟完全一致的行为。有时甚至需要自己实现一个与encodeURIComponent行为完全一致的函数。JSON 字符串化的坑JSON.stringify在默认情况下不会对对象的键进行排序。但有些安全要求高的签名方案会先对对象的键排序再stringify以确保序列化结果唯一。这一点必须通过 JS 调试确认。Python 的json.dumps(body, sort_keysTrue)可以实现键排序。签名原文的格式分隔符是|还是、\n或者干脆没有参数部分是否包含?前缀这些细节一个都不能错。最好的方法就是在 JS 调试阶段把最终拼接好的signMessage字符串完整地打印并复制出来然后在 Python 中严格按照这个字符串进行比对。时间戳的精度是秒10位还是毫秒13位这直接影响签名原文。从 JS 的Date.now()获取的是毫秒。密钥的格式密钥是字符串还是十六进制字符串在 JS 中CryptoJS.HmacSHA512(message, key)的key参数可以是字符串或 WordArray。在 Python 中hmac.new(key, msg, digestmod)的key需要是bytes。确保转换一致。调试流程建议在浏览器中完成一次成功请求从 Network 面板记录下URL、Params、Body、Headers中的X-Timestamp和X-Sign。在浏览器 Console 中使用你找到的 JS 函数或直接执行相关代码片段用记录下来的数据手动计算一次签名确认能复现出相同的X-Sign。将同样的输入数据URL,Params,Body,Timestamp填入你的 Python 代码。运行 Python 代码比较输出的签名与浏览器中的签名。如果不一致开启 Python 代码的调试输出打印出每一步的中间结果如排序后的参数字符串、拼接前的各部分、最终的签名原文与你在浏览器 Console 中打印的中间结果进行逐字对比。差异点就是问题所在。6. 常见问题、排查技巧与进阶对抗即使按照上述流程你也可能会遇到各种问题。这里汇总了一些常见坑点和排查思路。6.1 签名校验失败原因速查表问题现象可能原因排查思路Python 生成的签名与浏览器不一致1. 签名原文拼接格式错误。2. 参数排序规则不一致。3. URL 编码方式不同。4. JSON 序列化不一致空格、键序。5. 时间戳格式或值不对。6. 密钥错误。1.逐段对比在 JS 和 Python 中分别打印出拼接前的每一部分url_path, sorted_params_str, body_str, timestamp进行严格比对。2.编码验证对同一个字符串value分别用 JSencodeURIComponent(value)和 Pythonurllib.parse.quote(value, safe)打印结果。3.最终原文比对确保 JS 和 Python 中用于计算 HMAC 的最终字符串完全一致包括每个字符、空格、标点。签名偶尔成功大部分失败1. 签名原文中包含动态变化的值如随机数 nonce但未正确捕获。2. 密钥是动态获取的已过期或未更新。3. 服务器时间窗校验严格本地时钟不同步。1. 检查除了 timestamp 外是否还有nonce、requestId等动态字段参与签名。2. 确认密钥的获取和刷新机制。3. 同步本地时间到网络时间。请求返回 403/401但签名看起来正确1. 签名放在了错误的请求头字段。2. 缺少其他必要的认证头如Cookie中的 token。3. 请求被其他反爬策略拦截如 IP 频率限制、行为检测。4. 服务器算法已更新逆向逻辑过期。1. 用抓包工具如 Fiddler, Charles拦截一次浏览器正常请求对比你的 Python 请求与其在所有头字段、Cookie、请求体上的差异。2. 确保携带了有效的登录态 Cookie 或 Token。3. 降低请求频率模拟人类操作间隔。4. 重新进行逆向分析确认算法是否改变。无法在 JS 中找到明显的加密函数1. 代码被高度混淆和压缩。2. 加密逻辑被隐藏在 WebAssembly 或异步加载的模块中。3. 使用了非标准的加密库或自定义算法。1. 尝试使用“Hook”技术。在 Console 中重写关键函数如CryptoJS.HmacSHA512或Date.now()在其被调用时打印参数和堆栈。例如let _originalHmac CryptoJS.HmacSHA512; CryptoJS.HmacSHA512 function(m, k) { console.trace(Hmac called:, m, k); return _originalHmac(m, k); };2. 搜索特征字节码或字符串常量。3. 关注网络请求的 Initiator 调用栈可能指向一个很小的、核心的模块文件。6.2 进阶对抗当简单逆向失效时企查查作为大型商业平台其反爬策略是持续升级的。你可能会遇到更复杂的情况代码混淆与反调试代码被压缩成单行变量名被替换并添加了反调试逻辑例如在开发者工具打开时触发无限 debugger 或跳转。应对方法使用条件断点绕过无限 debugger或者使用浏览器插件如Tampermonkey注入脚本提前禁用反调试代码对于混淆耐心分析核心数据流和操作序列不纠结于变量名。环境检测签名算法可能依赖浏览器环境生成的一些指纹如navigator.userAgent,screen.width, 某个 Canvas 指纹等。这些值会被拼接进签名原文。应对方法在 Python 中需要模拟相同的环境值通常可以从浏览器的一次成功请求中复制这些固定值。密钥动态化密钥不是硬编码而是每次会话通过一个加密的接口获取或者由前端代码根据一些固定种子实时计算。应对方法逆向密钥的获取或计算流程。如果是接口获取需要先模拟请求那个接口如果是计算则需要复现计算逻辑。这可能涉及更复杂的 JS 逆向甚至包括 RSA 解密等。算法变种可能不是标准的 HMAC而是自定义的哈希拼接方式或者先对原文做了一次哈希再用 HMAC。应对方法仔细分析 JS 代码中所有的哈希函数调用CryptoJS.MD5,CryptoJS.SHA256等看它们是如何被组合使用的。6.3 工程化与维护建议模块化封装将签名生成类独立成一个模块如qichacha_signer.py方便在爬虫项目中调用和维护。配置化将密钥、签名原文格式、编码方式等变量提取为配置项这样当网站更新时只需修改配置而无需深入改动代码逻辑。日志与监控在签名函数中加入详细的 DEBUG 级别日志记录每次签名的输入和输出。当请求失败时可以快速定位是签名问题还是其他问题。自动更新探测可以定期用一组固定参数运行签名生成与一个“已知正确”的签名对比。如果不一致则触发告警提示算法可能已更新。遵守规则始终牢记逆向技术应用于学习、测试和接口自动化等合法合规场景。在实际使用中务必尊重网站的robots.txt协议控制请求频率避免对目标服务器造成过大压力。逆向分析是一个需要耐心、细心和逻辑思维的过程。每一次成功的逆向不仅让你获得所需的数据更能极大地提升你对 Web 安全、前端工程和密码学应用的理解。企查查的 HMAC-SHA512 签名是一个经典的案例掌握了它你再遇到类似的美团、淘宝、抖音等平台的签名机制时就有了可以套用的方法论和解决问题的底气。
逆向企查查动态HMAC-SHA512签名:从原理到Python复现的爬虫实战
发布时间:2026/6/22 13:28:46
1. 项目概述当爬虫遇上动态加密参数做数据采集的朋友尤其是爬虫工程师对“企查查”这个名字一定不陌生。作为国内领先的企业信息查询平台它不仅是商业调查的利器也常常是我们获取结构化企业数据的“数据源”。然而当你像往常一样打开浏览器开发者工具准备抓取一个企业详情页的接口时可能会发现事情没那么简单。一个看似普通的GET或POST请求其请求头里却多了一个神秘的字段比如X-Hmac-Signature它的值是一长串看似随机、每次请求都在变化的字符。这就是我们今天要面对的核心挑战动态 HMAC-SHA512 参数。这个参数不是摆设它是企查查后端服务器验证请求合法性的“门禁卡”。服务器会用同样的算法和密钥对请求的特定部分如 URL、时间戳、请求体等进行计算得到一个签名值然后与你请求头里带来的签名进行比对。如果一致说明请求是“自己人”发的放行如果不一致或者干脆没有那对不起直接返回 403 或 401 错误你的爬虫程序瞬间“哑火”。这种机制我们通常称之为“请求签名”或“API 签名认证”是反爬虫体系中非常有效且常见的一环它从请求的“身份”层面进行拦截比单纯检查User-Agent、Cookie要高级得多。面对这种情况常规的请求头复制大法从浏览器直接复制cURL命令完全失效因为那个签名是动态的、一次性的。我们必须深入前端 JavaScript 代码找到生成这个签名的算法逻辑、密钥以及参与计算的原始数据我们称之为“签名原文”然后用 Python 或其他语言将其复现出来。这个过程就是JS 逆向。本次实战的目标非常明确逆向分析企查查 Web 端或 App 端通常指其 H5 页面或小程序网络请求中用于签名认证的动态 HMAC-SHA512 参数的生成逻辑并成功用 Python 代码模拟生成从而让我们的爬虫程序能够持续、稳定地发起被后端认可的合法请求。这不仅仅是一次技术破解更是一次完整的学习路径你需要理解 HMAC 算法的原理需要熟练使用浏览器开发者工具进行断点调试需要具备一定的 JavaScript 代码阅读和逻辑分析能力最后还需要将 JS 代码精准地翻译成 Python 代码。整个过程是对爬虫工程师综合能力的一次绝佳锻炼。2. 核心思路与逆向策略拆解在开始动手之前我们不能像无头苍蝇一样乱撞。一个清晰的逆向策略能事半功倍。企查查这类商业站点的反爬策略通常不是单一的签名算法可能嵌套在复杂的代码混淆和流程中。我们的核心思路可以概括为“定位 - 分析 - 还原 - 复现”四步走。2.1 逆向目标与核心问题定义首先我们必须明确逆向的具体目标。一个动态签名参数通常包含以下几个关键要素算法类型确认是 HMAC-SHA512而不是 MD5、SHA256 或其他变种。密钥Secret Key这是签名的核心机密可能硬编码在 JS 中也可能通过更复杂的方式动态获取。签名原文Message即被签名的原始字符串。它由哪些部分按照什么顺序和格式拼接而成常见部分包括HTTP 方法GET/POST。请求的 API 路径如/api/company/detail。排序后的查询参数Query String。排序后的请求体Body如果是 POST 且为x-www-form-urlencoded或json。当前时间戳Timestamp。随机数Nonce。其他固定字符串或版本号。输出格式计算出的哈希值是直接输出还是经过 Base64 或 Hex十六进制编码在请求头里是以什么形式呈现的我们的任务就是在浩如烟海的 JavaScript 代码中找到负责组装“签名原文”和调用 HMAC-SHA512 算法的函数并理清上述所有要素。2.2 工具准备与逆向环境工欲善其事必先利其器。以下是本次实战的必备工具清单浏览器Google Chrome或Microsoft Edge基于 Chromium。它们的开发者工具F12是我们最主要的战场。开发者工具关键面板Network网络记录所有网络请求查看请求头和响应筛选出我们目标 API 的请求。Sources源代码用于查看、搜索和调试 JavaScript 文件。可以设置断点、单步执行是逆向分析的核心。Console控制台可以执行 JavaScript 代码片段用于测试我们找到的函数或变量。浏览器插件EditThisCookie或同类工具方便地查看和编辑 Cookie有时签名会与 Cookie 中的某个session或token关联。ReRes或Local Overrides功能可以映射线上 JS 文件到本地修改后的版本用于持久化我们的调试代码或绕过某些检测但本次实战可能不需要。代码编辑与调试工具VS Code或PyCharm用于编写和调试我们的 Python 复现代码。Node.js有时为了验证算法我们可能需要先在 Node.js 环境下运行我们提取的 JS 代码片段确保逻辑正确。辅助分析网站https://tool.lu/或https://www.sojson.com/用于在线格式化、美化被压缩混淆的 JS 代码虽然浏览器 Sources 面板也自带格式化功能点击{}图标但有时在线工具更强大。https://www.base64encode.org/等用于快速验证编解码结果。2.3 通用逆向流程与心法面对混淆的 JS 代码不要慌张。遵循以下流程一步步拆解网络抓包定位特征在浏览器中打开企查查网站进行能触发目标 API 的操作如搜索公司、查看详情。在 Network 面板中仔细查看该请求的Headers。找到那个可疑的动态参数记下它的名字例如x-sign、authorization、signature。同时注意观察请求的Initiator列它有时会指向发起这个请求的 JS 文件这是一个重要的线索入口。全局搜索缩小范围在 Sources 面板中使用CtrlShiftF进行全局搜索。关键词可以是动态参数名如x-sign。算法名如HMAC、SHA512、createHmacNode.js Crypto 模块方法、CryptoJS.HmacSHA512前端常用库。可能的关键常量如secret、key、sign。请求 URL 的一部分。格式化代码设置断点找到包含关键词的 JS 文件后点击左下角的{}按钮进行代码美化格式化。然后在疑似生成签名的函数调用处例如一个sign函数或一个axios.interceptors.request.use拦截器打上断点。动态调试追踪数据流重新触发请求代码会在断点处暂停。此时利用Scope面板查看局部变量和闭包变量利用Call Stack面板查看函数调用栈。重点关注传入函数的参数是什么函数内部如何拼接字符串最终返回的签名是什么。通过单步执行F10、步入F11、步出ShiftF11像侦探一样追踪数据的流向。提取关键逻辑一旦理清了签名原文的拼接规则和加密调用就将相关的 JS 代码片段提取出来。这可能包括一个工具函数、一个对象配置或者几行关键的逻辑。本地验证与翻译将提取的 JS 代码在浏览器 Console 中或 Node.js 环境中运行用已知的一次请求数据从 Network 面板复制作为输入验证是否能输出与请求头中一致的签名。验证成功后开始将其翻译成 Python 代码。Python 的hmac和hashlib库是标准库完全可以实现相同的功能。注意现代网站普遍使用 Webpack 等打包工具代码模块化严重函数和变量名可能被压缩成单个字母。这时搜索算法库的调用如CryptoJS或观察网络请求的发起栈Initiator会更加有效。同时签名可能不在主业务代码里而是在一个通用的“请求工具”或“SDK”文件中。3. 深入原理HMAC-SHA512 与请求签名机制在动手逆向之前我们有必要从原理上理解对手。这不仅有助于分析更能让我们在复现时避免低级错误。3.1 HMAC 算法消息认证码的核心HMACHash-based Message Authentication Code即基于哈希的消息认证码。它不是一种独立的哈希算法而是一种利用现有哈希函数如 MD5, SHA1, SHA256, SHA512来构造“消息认证码”的技术。它的核心思想是在计算哈希之前将消息Message和一个密钥Secret Key混合起来。这样只有拥有相同密钥的双方才能对相同的消息计算出相同的哈希值即认证码。这解决了单纯哈希如 SHA512无法验证消息来源和完整性的问题。HMAC 的计算过程有标准定义大致如下以伪代码表示key 处理后的密钥如果密钥过长则先哈希过短则补零 opad 外部填充常量0x5c 重复多次 ipad 内部填充常量0x36 重复多次 // 计算内部哈希 inner_hash hash((key XOR ipad) message) // 计算最终 HMAC hmac hash((key XOR opad) inner_hash)幸运的是我们几乎不需要自己实现这个过程无论是 JavaScript 的CryptoJS.HmacSHA512(message, key)还是 Python 的hmac.new(key, message, hashlib.sha512).digest()都封装好了这个标准流程。为什么是 SHA512SHA512 是 SHA-2 家族的一员输出长度为 512 位64字节比 SHA256 更长理论上更安全碰撞概率更低。对于高安全要求的商业 API使用 SHA512 是合理的。3.2 请求签名构建防伪“信封”企查查的 API 签名可以看作是把一次 HTTP 请求打包成一个带有防伪 seal密封章的信封。收集信封内容签名原文把这次请求的核心信息收集起来比如“在2023-10-27 14:30:00时间戳我要用 GET 方法访问/api/v4/company/getDetail这个地址查询参数是id123456”。这些信息按固定顺序拼接成一个字符串。这个顺序非常重要服务器和客户端必须严格一致。盖上密封章HMAC计算用只有服务器和合法客户端知道的“密钥”Secret Key作为印泥对上面拼接好的字符串信封内容进行 HMAC-SHA512 计算得到一串二进制哈希值。贴上密封条编码与传输二进制哈希值不方便在 HTTP 文本协议中传输所以通常会进行 Base64 或十六进制Hex编码变成一个字符串。最后把这个字符串放在 HTTP 请求头的一个特定字段如X-Sign里随请求一起发送给服务器。服务器验章服务器收到请求后做完全相同的事情用同样的密钥按照同样的规则拼接签名原文计算 HMAC-SHA512然后比较计算结果和请求头里带来的签名是否一致。一致则通过不一致则拒绝。这种机制的强大之处在于防篡改如果攻击者中途修改了请求的任何部分如参数由于签名原文变了计算出的签名会完全不同服务器会拒绝。防重放签名原文里通常包含时间戳Timestamp和随机数Nonce。服务器可以检查时间戳是否在可接受的时间窗口内如±5分钟并记录近期使用过的 Nonce从而防止同一个请求被重复发送重放攻击。身份验证只有拥有正确密钥的客户端才能生成有效的签名实现了对客户端的认证。我们的逆向就是要破解这个“信封”的封装规则和“印泥”密钥。4. 实战逆向定位并分析企查查签名逻辑理论准备就绪现在让我们进入实战环节。由于企查查的具体代码会随时间更新以下过程是一种通用的、方法论层面的演示你需要根据当时网站的实际情况进行调整。4.1 第一步网络抓包与特征确认打开 Chrome 浏览器进入企查查官网 (www.qichacha.com)。按F12打开开发者工具切换到Network面板。确保勾选了Preserve log保留日志。在网站上执行一个明确的搜索操作例如在搜索框输入一个公司名点击搜索。在 Network 面板中你会看到一系列请求。寻找返回公司列表或详情的 API 请求。通常它们的 URL 会包含/api/、/v4/、/search等关键词响应类型Type是xhr或fetch。点击这个目标请求在右侧的Headers选项卡中向下滚动到Request Headers部分。仔细寻找看起来像加密字符串的字段。常见的嫌疑字段名有X-SignX-SignatureAuthorization(可能包含Bearer以外的自定义方案)X-Hmac-SignatureQC-SignToken(但可能是静态的) 例如你可能会看到X-Sign: V2RGeVFtUXlNVFE0TURrM09UazVNRGt5TlRrM09UazVPVE13TURrM09Uaz0这串字符末尾有强烈暗示它是 Base64 编码的。记下这个字段名和一次具体的值。同时记录下这次请求的完整 URL、方法GET/POST、以及所有的 Query Parameters 和 Request Payload如果有。4.2 第二步全局搜索与初步定位切换到Sources面板。按CtrlShiftF打开全局搜索框。输入我们上一步找到的签名头字段名比如X-Sign进行搜索。注意匹配大小写。如果直接搜索字段名没有结果可能是因为代码被压缩字段名被作为字符串常量放在了别处。可以尝试搜索sign小写或者hmac、sha512。更有效的方法是回到 Network 面板点击目标请求在右侧的Initiator选项卡里查看这个请求是由哪个 JS 文件发起的。点击那个文件名可以直接跳转到 Sources 面板的对应文件。跳转后首先点击左下角的{}按钮Pretty-print来格式化这份可能被压缩成一行的代码。4.3 第三步关键代码分析与断点调试假设我们通过搜索hmac或sha512在一个名为app.xxxxxx.js的文件里找到了疑似代码。格式化后我们可能会看到类似下面的结构这是模拟的、简化后的代码// 模拟代码非真实企查查代码 function generateSign(url, params, data, timestamp) { // 1. 对参数进行排序并拼接 var sortedParams Object.keys(params).sort().map(function(key) { return key encodeURIComponent(params[key]); }).join(); // 2. 如果有请求体也处理假设是JSON var dataStr ; if (data Object.keys(data).length 0) { dataStr JSON.stringify(data); } // 3. 拼接签名原文 var signString [url, sortedParams, dataStr, timestamp].join(|); console.log(Sign String:, signString); // 调试用 // 4. 使用 CryptoJS 计算 HMAC-SHA512 var secretKey aVerySecretKey123456; // 密钥可能在这里也可能从别处获取 var hash CryptoJS.HmacSHA512(signString, secretKey); // 5. 将二进制哈希转换为 Base64 字符串 var signBase64 CryptoJS.enc.Base64.stringify(hash); return signBase64; } // 在请求拦截器中调用 axios.interceptors.request.use(function(config) { var timestamp Date.now().toString(); config.headers[X-Timestamp] timestamp; // 假设我们从 config 中提取必要信息 var sign generateSign(config.url, config.params, config.data, timestamp); config.headers[X-Sign] sign; return config; });分析要点找到入口我们找到了generateSign函数和请求拦截器。这就是签名的生成入口。理解逻辑signString的拼接方式是url|sortedParams|dataStr|timestamp。这是签名原文的格式是逆向的核心成果之一。它使用了CryptoJS.HmacSHA512函数确认了算法。密钥secretKey硬编码在函数里aVerySecretKey123456。在实际中密钥可能不在这里而是通过某个接口获取或者是从一个更大的配置对象中读取甚至是通过更复杂的算法动态生成。这是逆向的另一个关键点。最终输出做了 Base64 编码。设置断点在generateSign函数的第一行和return行打上断点。触发调试回到网页再次触发搜索请求。代码会在断点处暂停。观察数据在Scope面板查看url,params,data,timestamp的值确认它们是否与 Network 面板中看到的请求信息一致。单步执行F10观察sortedParams和signString的生成过程。在 Console 中打印signString复制下来。继续执行得到signBase64。与 Network 面板中请求头里的X-Sign值对比如果一致那么恭喜你核心逻辑找到了实操心得在实际逆向中代码往往被严重混淆变量名可能是a,b,c,t,e等。这时候不要纠结于变量名而要关注数据流和操作序列。比如看到Object.keys(t).sort()就要意识到这是在排序看到.map(...).join()就要意识到这是在拼接查询字符串看到CryptoJS.HmacSHA512或e.createHmac(sha512, n).update(s).digest(base64)这样的调用就要知道这是加密核心。抓住这些关键操作节点就能像拼图一样还原出逻辑。4.4 第四步密钥的寻找密钥是签名的灵魂。如果它像上面例子一样硬编码在代码里那是最简单的情况。但更多时候它会被隐藏全局变量可能是一个在更早初始化的全局变量如window._secretKey或__QC_CONFIG__.key。接口获取可能在页面初始化时通过一个不显眼的 API 请求从服务器获取然后存储在内存或sessionStorage中。你需要在 Network 中寻找更早的、可能返回密钥的请求。本地计算可能由页面的某些固定信息如用户ID、设备指纹通过一个固定算法计算得出。这需要你逆向计算过程。分段存储密钥被拆分成多个部分分散在代码的不同位置使用时再拼接。寻找密钥的技巧在格式化后的 JS 代码中搜索secret、key、appkey、appSecret等关键词。在generateSign函数内部对secretKey变量右键点击 “Find all references”查看它在哪里被定义和赋值。在 Console 中尝试输入可能存在的全局变量名如window.QC_SECRET看是否有返回值。如果密钥来自接口在 Network 中过滤XHR/Fetch请求查看响应体寻找包含长字符串的字段。5. Python 复现从 JS 逻辑到可运行代码一旦我们在浏览器中成功验证了签名逻辑下一步就是用 Python 将其复现集成到我们的爬虫程序中。5.1 环境准备与核心库确保你的 Python 环境安装了必要的库。我们主要使用标准库。# 通常不需要额外安装hmac 和 hashlib 是标准库 # 但为了处理 URL 编码和 JSON我们会用到 urllib 和 json它们也是标准库。5.2 代码复现详解假设我们逆向出的逻辑如下基于之前的模拟代码签名原文格式{url}|{sorted_query_string}|{request_body_json}|{timestamp}请求体data为空时用空字符串表示。查询参数params需要按键名升序排序并按keyvalue用连接value需要做 URL 编码。密钥secret_key为固定字符串aVerySecretKey123456。使用 HMAC-SHA512 算法。输出结果进行 Base64 编码。下面是用 Python 实现的代码import hmac import hashlib import base64 import time import json from urllib.parse import urlencode, quote_plus class QichachaSignGenerator: def __init__(self, secret_keyaVerySecretKey123456): 初始化签名生成器 :param secret_key: 从JS逆向中获取的密钥 self.secret_key secret_key.encode(utf-8) # 密钥需要转换为bytes def generate_signature(self, method, url_path, paramsNone, bodyNone, timestampNone): 生成企查查请求签名 :param method: HTTP方法如 GET, POST :param url_path: API路径如 /api/v4/company/getDetail :param params: 字典查询参数 :param body: 字典请求体JSON格式 :param timestamp: 时间戳毫秒如果为None则使用当前时间 :return: 计算得到的签名字符串 (Base64) if timestamp is None: timestamp int(time.time() * 1000) # 当前时间戳毫秒 timestamp_str str(timestamp) # 1. 处理查询参数 sorted_query_str if params and isinstance(params, dict): # 按键名升序排序 sorted_params sorted(params.items(), keylambda x: x[0]) # 拼接成 k1v1k2v2 格式并对value进行URL编码 encoded_items [] for key, value in sorted_params: # 注意这里使用 quote_plus 对值进行编码模拟JS中的 encodeURIComponent # encodeURIComponent 会将空格转为 %20而 quote_plus 会转为 。 # 企查查后端可能使用其中一种需要根据JS代码确认。 # 通常更安全的做法是使用 quote(value, safe)它对应 encodeURIComponent。 # 这里先假设使用 quote_plus实际需调试确定。 encoded_value quote_plus(str(value)) if value is not None else encoded_items.append(f{key}{encoded_value}) sorted_query_str .join(encoded_items) # 2. 处理请求体 body_str if body and isinstance(body, dict): # 将body字典转换为JSON字符串。注意JS中JSON.stringify会对键进行排序吗 # 默认不会但有些实现会先对body的键排序再stringify。 # 我们需要根据逆向结果确定。假设这里不需要排序直接转换。 body_str json.dumps(body, separators(,, :), ensure_asciiFalse) # 紧凑格式无空格 # 如果逆向发现JS中对body的键也排序了则需要 # body_str json.dumps(body, sort_keysTrue, separators(,, :), ensure_asciiFalse) # 3. 拼接签名原文 # 根据逆向结果格式为 url|sorted_params|body|timestamp # 注意url 是 path 还是 full url 模拟代码中是 config.url可能包含查询参数。 # 但我们的 sorted_query_str 已经单独处理了所以这里的 url 应该是不带查询参数的路径。 # 需要根据JS逻辑确认。假设是 path。 sign_message_parts [ url_path, # API路径 sorted_query_str, # 排序后的查询字符串 body_str, # 请求体JSON字符串 timestamp_str # 时间戳 ] sign_message |.join(sign_message_parts) print(f[DEBUG] 签名原文: {sign_message}) # 调试时打印正式使用可移除 # 4. 计算 HMAC-SHA512 # 注意sign_message 需要转换为 bytes message_bytes sign_message.encode(utf-8) hmac_obj hmac.new(self.secret_key, message_bytes, hashlib.sha512) # 5. 获取二进制摘要并进行Base64编码 digest_bytes hmac_obj.digest() signature_b64 base64.b64encode(digest_bytes).decode(utf-8) return signature_b64, timestamp_str # 使用示例 if __name__ __main__: signer QichachaSignGenerator(secret_keyaVerySecretKey123456) # 替换为真实密钥 # 模拟一次请求 method GET url_path /api/v4/company/getDetail params { companyId: 123456789, pageIndex: 1, pageSize: 20 } body None # GET请求通常无body # body {someKey: someValue} # 如果是POST请求 signature, ts signer.generate_signature(method, url_path, params, body) print(f生成的时间戳: {ts}) print(f生成的签名: {signature}) # 构造请求头 headers { X-Timestamp: ts, X-Sign: signature, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., # ... 其他必要头信息 } print(f请求头示例: {headers})5.3 关键细节与调试技巧URL 编码的坑JavaScript 的encodeURIComponent和 Python 的urllib.parse.quote或quote_plus在处理空格、加号等字符时略有不同。如果签名校验失败首先检查这里。一个稳妥的方法是在 JS 调试时把即将被编码的字符串和编码后的结果打印出来然后在 Python 中模拟完全一致的行为。有时甚至需要自己实现一个与encodeURIComponent行为完全一致的函数。JSON 字符串化的坑JSON.stringify在默认情况下不会对对象的键进行排序。但有些安全要求高的签名方案会先对对象的键排序再stringify以确保序列化结果唯一。这一点必须通过 JS 调试确认。Python 的json.dumps(body, sort_keysTrue)可以实现键排序。签名原文的格式分隔符是|还是、\n或者干脆没有参数部分是否包含?前缀这些细节一个都不能错。最好的方法就是在 JS 调试阶段把最终拼接好的signMessage字符串完整地打印并复制出来然后在 Python 中严格按照这个字符串进行比对。时间戳的精度是秒10位还是毫秒13位这直接影响签名原文。从 JS 的Date.now()获取的是毫秒。密钥的格式密钥是字符串还是十六进制字符串在 JS 中CryptoJS.HmacSHA512(message, key)的key参数可以是字符串或 WordArray。在 Python 中hmac.new(key, msg, digestmod)的key需要是bytes。确保转换一致。调试流程建议在浏览器中完成一次成功请求从 Network 面板记录下URL、Params、Body、Headers中的X-Timestamp和X-Sign。在浏览器 Console 中使用你找到的 JS 函数或直接执行相关代码片段用记录下来的数据手动计算一次签名确认能复现出相同的X-Sign。将同样的输入数据URL,Params,Body,Timestamp填入你的 Python 代码。运行 Python 代码比较输出的签名与浏览器中的签名。如果不一致开启 Python 代码的调试输出打印出每一步的中间结果如排序后的参数字符串、拼接前的各部分、最终的签名原文与你在浏览器 Console 中打印的中间结果进行逐字对比。差异点就是问题所在。6. 常见问题、排查技巧与进阶对抗即使按照上述流程你也可能会遇到各种问题。这里汇总了一些常见坑点和排查思路。6.1 签名校验失败原因速查表问题现象可能原因排查思路Python 生成的签名与浏览器不一致1. 签名原文拼接格式错误。2. 参数排序规则不一致。3. URL 编码方式不同。4. JSON 序列化不一致空格、键序。5. 时间戳格式或值不对。6. 密钥错误。1.逐段对比在 JS 和 Python 中分别打印出拼接前的每一部分url_path, sorted_params_str, body_str, timestamp进行严格比对。2.编码验证对同一个字符串value分别用 JSencodeURIComponent(value)和 Pythonurllib.parse.quote(value, safe)打印结果。3.最终原文比对确保 JS 和 Python 中用于计算 HMAC 的最终字符串完全一致包括每个字符、空格、标点。签名偶尔成功大部分失败1. 签名原文中包含动态变化的值如随机数 nonce但未正确捕获。2. 密钥是动态获取的已过期或未更新。3. 服务器时间窗校验严格本地时钟不同步。1. 检查除了 timestamp 外是否还有nonce、requestId等动态字段参与签名。2. 确认密钥的获取和刷新机制。3. 同步本地时间到网络时间。请求返回 403/401但签名看起来正确1. 签名放在了错误的请求头字段。2. 缺少其他必要的认证头如Cookie中的 token。3. 请求被其他反爬策略拦截如 IP 频率限制、行为检测。4. 服务器算法已更新逆向逻辑过期。1. 用抓包工具如 Fiddler, Charles拦截一次浏览器正常请求对比你的 Python 请求与其在所有头字段、Cookie、请求体上的差异。2. 确保携带了有效的登录态 Cookie 或 Token。3. 降低请求频率模拟人类操作间隔。4. 重新进行逆向分析确认算法是否改变。无法在 JS 中找到明显的加密函数1. 代码被高度混淆和压缩。2. 加密逻辑被隐藏在 WebAssembly 或异步加载的模块中。3. 使用了非标准的加密库或自定义算法。1. 尝试使用“Hook”技术。在 Console 中重写关键函数如CryptoJS.HmacSHA512或Date.now()在其被调用时打印参数和堆栈。例如let _originalHmac CryptoJS.HmacSHA512; CryptoJS.HmacSHA512 function(m, k) { console.trace(Hmac called:, m, k); return _originalHmac(m, k); };2. 搜索特征字节码或字符串常量。3. 关注网络请求的 Initiator 调用栈可能指向一个很小的、核心的模块文件。6.2 进阶对抗当简单逆向失效时企查查作为大型商业平台其反爬策略是持续升级的。你可能会遇到更复杂的情况代码混淆与反调试代码被压缩成单行变量名被替换并添加了反调试逻辑例如在开发者工具打开时触发无限 debugger 或跳转。应对方法使用条件断点绕过无限 debugger或者使用浏览器插件如Tampermonkey注入脚本提前禁用反调试代码对于混淆耐心分析核心数据流和操作序列不纠结于变量名。环境检测签名算法可能依赖浏览器环境生成的一些指纹如navigator.userAgent,screen.width, 某个 Canvas 指纹等。这些值会被拼接进签名原文。应对方法在 Python 中需要模拟相同的环境值通常可以从浏览器的一次成功请求中复制这些固定值。密钥动态化密钥不是硬编码而是每次会话通过一个加密的接口获取或者由前端代码根据一些固定种子实时计算。应对方法逆向密钥的获取或计算流程。如果是接口获取需要先模拟请求那个接口如果是计算则需要复现计算逻辑。这可能涉及更复杂的 JS 逆向甚至包括 RSA 解密等。算法变种可能不是标准的 HMAC而是自定义的哈希拼接方式或者先对原文做了一次哈希再用 HMAC。应对方法仔细分析 JS 代码中所有的哈希函数调用CryptoJS.MD5,CryptoJS.SHA256等看它们是如何被组合使用的。6.3 工程化与维护建议模块化封装将签名生成类独立成一个模块如qichacha_signer.py方便在爬虫项目中调用和维护。配置化将密钥、签名原文格式、编码方式等变量提取为配置项这样当网站更新时只需修改配置而无需深入改动代码逻辑。日志与监控在签名函数中加入详细的 DEBUG 级别日志记录每次签名的输入和输出。当请求失败时可以快速定位是签名问题还是其他问题。自动更新探测可以定期用一组固定参数运行签名生成与一个“已知正确”的签名对比。如果不一致则触发告警提示算法可能已更新。遵守规则始终牢记逆向技术应用于学习、测试和接口自动化等合法合规场景。在实际使用中务必尊重网站的robots.txt协议控制请求频率避免对目标服务器造成过大压力。逆向分析是一个需要耐心、细心和逻辑思维的过程。每一次成功的逆向不仅让你获得所需的数据更能极大地提升你对 Web 安全、前端工程和密码学应用的理解。企查查的 HMAC-SHA512 签名是一个经典的案例掌握了它你再遇到类似的美团、淘宝、抖音等平台的签名机制时就有了可以套用的方法论和解决问题的底气。