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 文件到本地修改后的版本,用于持久化我们的调试代码或绕过某些检测,但本次实战可能不需要。
- EditThisCookie或同类工具:方便地查看和编辑 Cookie,有时签名会与 Cookie 中的某个
- 代码编辑与调试工具:
- VS Code或PyCharm:用于编写和调试我们的 Python 复现代码。
- Node.js:有时为了验证算法,我们可能需要先在 Node.js 环境下运行我们提取的 JS 代码片段,确保逻辑正确。
- 辅助分析网站:
- https://tool.lu/或https://www.sojson.com/:用于在线格式化、美化被压缩混淆的 JS 代码,虽然浏览器 Sources 面板也自带格式化功能(点击
{}图标),但有时在线工具更强大。 - https://www.base64encode.org/等:用于快速验证编解码结果。
- https://tool.lu/或https://www.sojson.com/:用于在线格式化、美化被压缩混淆的 JS 代码,虽然浏览器 Sources 面板也自带格式化功能(点击
2.3 通用逆向流程与心法
面对混淆的 JS 代码,不要慌张。遵循以下流程,一步步拆解:
- 网络抓包,定位特征:在浏览器中打开企查查网站,进行能触发目标 API 的操作(如搜索公司、查看详情)。在 Network 面板中,仔细查看该请求的Headers。找到那个可疑的动态参数,记下它的名字(例如
x-sign、authorization、signature)。同时,注意观察请求的Initiator列,它有时会指向发起这个请求的 JS 文件,这是一个重要的线索入口。 - 全局搜索,缩小范围:在 Sources 面板中,使用
Ctrl+Shift+F进行全局搜索。关键词可以是:- 动态参数名,如
x-sign。 - 算法名,如
HMAC、SHA512、createHmac(Node.js Crypto 模块方法)、CryptoJS.HmacSHA512(前端常用库)。 - 可能的关键常量,如
secret、key、sign。 - 请求 URL 的一部分。
- 动态参数名,如
- 格式化代码,设置断点:找到包含关键词的 JS 文件后,点击左下角的
{}按钮进行代码美化(格式化)。然后在疑似生成签名的函数调用处(例如一个sign函数,或一个axios.interceptors.request.use拦截器)打上断点。 - 动态调试,追踪数据流:重新触发请求,代码会在断点处暂停。此时,利用Scope面板查看局部变量和闭包变量,利用Call Stack面板查看函数调用栈。重点关注传入函数的参数是什么,函数内部如何拼接字符串,最终返回的签名是什么。通过单步执行(F10)、步入(F11)、步出(Shift+F11),像侦探一样追踪数据的流向。
- 提取关键逻辑:一旦理清了签名原文的拼接规则和加密调用,就将相关的 JS 代码片段提取出来。这可能包括一个工具函数、一个对象配置,或者几行关键的逻辑。
- 本地验证与翻译:将提取的 JS 代码在浏览器 Console 中或 Node.js 环境中运行,用已知的一次请求数据(从 Network 面板复制)作为输入,验证是否能输出与请求头中一致的签名。验证成功后,开始将其翻译成 Python 代码。Python 的
hmac和hashlib库是标准库,完全可以实现相同的功能。
注意:现代网站普遍使用 Webpack 等打包工具,代码模块化严重,函数和变量名可能被压缩成单个字母。这时,搜索算法库的调用(如
CryptoJS)或观察网络请求的发起栈(Initiator)会更加有效。同时,签名可能不在主业务代码里,而是在一个通用的“请求工具”或“SDK”文件中。
3. 深入原理:HMAC-SHA512 与请求签名机制
在动手逆向之前,我们有必要从原理上理解对手。这不仅有助于分析,更能让我们在复现时避免低级错误。
3.1 HMAC 算法:消息认证码的核心
HMAC(Hash-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(),都封装好了这个标准流程。
为什么是 SHA512?SHA512 是 SHA-2 家族的一员,输出长度为 512 位(64字节),比 SHA256 更长,理论上更安全,碰撞概率更低。对于高安全要求的商业 API,使用 SHA512 是合理的。
3.2 请求签名:构建防伪“信封”
企查查的 API 签名,可以看作是把一次 HTTP 请求打包成一个带有防伪 seal(密封章)的信封。
- 收集信封内容(签名原文):把这次请求的核心信息收集起来,比如:“在2023-10-27 14:30:00(时间戳),我要用 GET 方法访问
/api/v4/company/getDetail这个地址,查询参数是id=123456”。这些信息按固定顺序拼接成一个字符串。这个顺序非常重要,服务器和客户端必须严格一致。 - 盖上密封章(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面板。
- 按
Ctrl+Shift+F打开全局搜索框。 - 输入我们上一步找到的签名头字段名,比如
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值对比,如果一致,那么恭喜你,核心逻辑找到了!
- 在Scope面板,查看
实操心得:在实际逆向中,代码往往被严重混淆,变量名可能是
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)需要按键名升序排序,并按
key=value用&连接,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_key='aVerySecretKey123456'): """ 初始化签名生成器 :param secret_key: 从JS逆向中获取的密钥 """ self.secret_key = secret_key.encode('utf-8') # 密钥需要转换为bytes def generate_signature(self, method, url_path, params=None, body=None, timestamp=None): """ 生成企查查请求签名 :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(), key=lambda x: x[0]) # 拼接成 k1=v1&k2=v2 格式,并对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_ascii=False) # 紧凑格式,无空格 # 如果逆向发现JS中对body的键也排序了,则需要: # body_str = json.dumps(body, sort_keys=True, separators=(',', ':'), ensure_ascii=False) # 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_key='aVerySecretKey123456') # 替换为真实密钥 # 模拟一次请求 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_keys=True)可以实现键排序。 - 签名原文的格式:分隔符是
|还是&、\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 签名是一个经典的案例,掌握了它,你再遇到类似的美团、淘宝、抖音等平台的签名机制时,就有了可以套用的方法论和解决问题的底气。