逆向HmacSHA512动态签名:破解企业数据平台反爬机制实战
2026/7/27 11:17:28 网站建设 项目流程

1. 项目概述:当爬虫遇上动态加密请求头

做爬虫的朋友,尤其是和数据查询、企业信息这类网站打交道比较多的,肯定对“某查查”这类平台不陌生。它们的数据价值高,反爬措施也相应地非常严密。最近在分析其中一个数据接口时,我发现它的请求头里藏着一个动态变化的加密参数,核心算法是HmacSHA512。这不再是简单的token或者sign加个时间戳就能糊弄过去的了,而是一种基于密钥的哈希消息认证码,安全性很高。

简单来说,服务器和前端约定了一个密钥,前端用这个密钥对特定的消息(比如请求参数、时间戳等)进行 HmacSHA512 计算,生成一个唯一的、不可伪造的签名,放在请求头里。服务器收到后,用同样的密钥和规则再算一遍,如果一致就放行,否则直接拒绝。这种机制下,你光知道加密算法是没用的,核心在于找到那个用于计算的密钥,以及生成签名的原始消息(我们常说的“明文”)是什么

这个实战的目标很明确:逆向分析前端 JavaScript 代码,定位到 HmacSHA512 加密函数,找出其密钥和明文拼接逻辑,最终在 Python 中复现整个签名生成过程,让我们的爬虫程序能够自主构造出合法的请求头,成功获取数据。整个过程,就是一场典型的“猫鼠游戏”,考验的是对前端代码逻辑的梳理能力和密码学知识的应用。

2. 核心思路与逆向切入点分析

面对一个经过混淆、压缩的复杂前端 JS 代码库,直接“硬看”无异于大海捞针。我们需要一套系统的方法来缩小搜索范围,快速定位目标。

2.1 从网络请求入手,确定攻击面

一切逆向的开始,都是浏览器开发者工具的 Network 面板。首先,正常操作网站,触发你想要抓取数据的那个 XHR 或 Fetch 请求。重点关注请求的Headers部分。

以本次目标为例,在请求头中,我找到了一个名为X-Signature的字段,它的值是一长串固定的 128 位十六进制字符串(HmacSHA512 的结果就是 512 位,即 64 字节,用十六进制表示就是 128 个字符)。同时,请求头里通常还会伴随一个X-Timestamp字段。这几乎是一个明确的信号:签名很可能由“时间戳+其他信息”通过 HmacSHA512 生成。

注意:不同的网站,签名参数名可能不同,常见的有signature,sign,x-sign,authorization等。时间戳参数名也可能是timestamp,x-ts等。关键是要找到那对看起来像是“数据”和“其哈希签名”的字段。

2.2 关键线索:搜索与断点设置

有了参数名,我们就可以在 JS 代码里进行搜索了。

  1. 全局搜索关键词:在 Sources 面板下,按Ctrl+Shift+F打开全局搜索。首先搜索X-SignatureX-Timestamp,看看它们在哪里被设置。很可能是在一个统一的请求拦截器或axios/fetch的请求配置函数里。
  2. 搜索算法名称:直接搜索HmacSHA512CryptoJS.HmacSHA512(如果用了 CryptoJS 库)、createHmac(Node.js 或 Web Crypto API 风格)。如果代码混淆严重,这些字符串可能被转义或拆分,可以尝试搜索SHA512Hmac等部分关键词。
  3. Hook 关键函数:如果搜索无果,说明函数名可能被混淆了。这时可以尝试“Hook”住设置请求头的函数。在 Console 中执行以下代码,然后重新触发请求:
    // 拦截所有请求头的设置操作 (function() { var originalSet = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.setRequestHeader = function(key, value) { console.log('Setting Header:', key, '=>', value); if (key.toLowerCase().includes('sign')) { // 重点关注签名头 console.trace('Stack trace for sign header'); // 打印调用栈 debugger; // 自动断点 } return originalSet.apply(this, arguments); }; })();
    X-Signature被设置时,debugger会触发断点,此时调用栈(Call Stack)会清晰地展示出是哪个函数调用了它,逆向之旅就从这里正式深入。

2.3 逆向环境准备:本地调试与代码格式化

定位到关键的加密函数后,你面对的很可能是一坨压缩到一行的代码。这时,Chrome 开发者工具自带的“Pretty Print”功能(那个{}图标)就是你的救星。它能将混淆的代码格式化,恢复一定的可读性。

但更复杂的情况是,代码可能被Webpack等打包工具模块化,或者使用了自执行的函数闭包,导致无法直接在一个文件里看到完整逻辑。这时,你需要:

  1. 将代码导出到本地:在 Sources 面板找到目标 JS 文件,右键选择Save as...保存到本地。
  2. 使用本地 IDE 分析:用 VSCode、WebStorm 等打开,利用其强大的搜索、跳转和代码折叠功能进行分析。对于特别复杂的混淆,可以尝试使用de4js等在线或离线的反混淆工具进行初步处理,但要注意,过度反混淆可能会破坏代码逻辑。
  3. 构建补环境调试:最稳妥的方法是在 Node.js 环境中,补全加密函数所依赖的浏览器环境(如window,document,navigator等对象)。这样你可以单独运行、修改和调试这个加密函数,验证你的逆向结果。这是高阶逆向的必备技能。

3. HmacSHA512 加密逻辑深度解析

经过一番搜索和断点调试,我们终于找到了加密的核心函数。假设我们找到的函数片段经过格式化后如下所示(这是一个高度简化和模拟的例子,真实代码会复杂得多):

function generateSignature(timestamp, path, queryString) { // 1. 拼接明文消息 var message = `v1|${timestamp}|${path}|${queryString}`; // 2. 获取密钥,密钥可能来自一个固定的字符串,也可能由其他函数动态生成 var secretKey = window._globalConfig.apiSecret; // 假设密钥存放在全局配置中 // 3. 使用 CryptoJS 进行 HmacSHA512 加密 var hash = CryptoJS.HmacSHA512(message, secretKey); // 4. 将结果转换为十六进制字符串 var signature = hash.toString(CryptoJS.enc.Hex); return signature; }

让我们逐行拆解这个逻辑,这直接关系到我们后续用 Python 复现的成功率。

3.1 明文(Message)的拼接规则

这是最容易出错的一步。message的拼接方式因网站而异,但无外乎以下几种元素的排列组合:

  • API 版本:如v1v2,可能硬编码在字符串里。
  • 时间戳:通常是毫秒级或秒级时间戳。务必注意服务器使用的是秒还是毫秒,这可以通过对比请求头中的X-Timestamp和当前时间推算出来。
  • 请求路径:即 URL 的路径部分,如/api/v4/company/search注意是否包含开头的斜杠
  • 查询字符串:即?后面的部分,如name=测试&page=1这里坑最多
    • 参数是否需要按字母顺序排序?
    • 参数值是否需要做 URL 编码?是编码前拼接还是编码后拼接?
    • 空参数是否参与拼接?
    • 是否包含?符号本身?
  • 请求体:对于 POST 请求,消息体(JSON 或 FormData)可能也需要参与签名。通常是将其字符串化(如JSON.stringify)后拼接。
  • 分隔符:常用|&\n:或者直接拼接。

实操心得:最可靠的方法是,在 JS 代码里找到拼接message的那行代码,用console.log打印出最终的message字符串。然后在 Python 里用同样的规则拼接,确保两个字符串完全一致,包括每一个空格和标点。你可以先写一个简单的 Node.js 脚本,把关键函数抠出来运行,打印中间变量进行验证。

3.2 密钥(Secret Key)的寻找

密钥是 Hmac 算法的灵魂。在上述例子中,它看似从window._globalConfig.apiSecret获取。但在逆向中,这个_globalConfig对象可能是在其他 JS 文件里定义,或者通过一个复杂的函数计算得来。

  • 搜索字符串:在全站 JS 代码中搜索apiSecretsecretKeyappSecret等关键词。
  • 跟踪变量:在加密函数处打上断点,然后观察secretKey这个变量的值。在 Chrome 的 Scope 面板或 Watch 面板里,可以查看它的具体内容。如果它是一个变量,可以右键点击选择 “Reveal in Sources panel” 来查看它在哪里被赋值。
  • 可能是计算值:有些网站的密钥并非明文存储,而是由appId、固定盐值(salt)和当前日期等元素通过某种哈希(如 MD5)计算得出。你需要逆向这个计算过程。

3.3 加密库的识别

前端实现 HmacSHA512 常见有以下几种方式:

  1. CryptoJS:最常用。特征是有CryptoJS.HmacSHA512调用,或者先CryptoJS.HmacSHA512.toString()
  2. Web Crypto API:现代浏览器原生支持。特征是用crypto.subtle.importKey,crypto.subtle.sign等函数,代码看起来更“原生”和异步。
  3. 自定义实现或小众库:较少见,需要你仔细分析其内部的哈希计算过程。

识别出使用的库,才能在用 Python 复现时选择对应的实现方式。CryptoJS 的 HmacSHA512 和 Python 的hmac.new(key, msg, hashlib.sha512).hexdigest()在结果上是标准且互通的,前提是密钥和消息处理一致。

4. Python 复现完整流程与代码实现

假设我们已经通过逆向分析,100% 确定了以下信息:

  • 明文规则f"v1|{timestamp}|{path}|{sorted_query_string}"
    • timestamp: 秒级时间戳。
    • path: 请求路径,如/api/v4/search
    • sorted_query_string: 将查询参数字典按 key 排序后,拼接成key1=value1&key2=value2的格式,值需要经过 URL 编码
  • 密钥:一个固定的字符串,例如"this_is_my_secret_key_2024"
  • 加密方式:CryptoJS.HmacSHA512。

下面我们用 Python 来复现。

4.1 环境准备与依赖安装

确保你的 Python 环境已安装必要的库。我们主要使用hashlibhmac,它们是 Python 标准库的一部分,无需额外安装。但为了处理 URL 编码和请求,我们也会用到urllibrequests

# 主要用到的内置库,无需安装 import hashlib import hmac import time from urllib.parse import quote, urlencode # 用于最终发送请求,需要安装:pip install requests import requests

4.2 签名生成函数详解

这是最核心的部分,每一步都必须和前端逻辑严丝合缝。

def generate_signature(secret_key: str, timestamp: int, path: str, params: dict) -> str: """ 根据逆向分析的规则生成 HmacSHA512 签名。 Args: secret_key: 逆向得到的密钥字符串。 timestamp: 秒级时间戳。 path: API请求路径,如 '/api/v4/search'。 params: 请求参数字典,如 {'name': '测试', 'page': 1}。 Returns: 128位的十六进制签名字符串。 """ # 1. 对参数进行排序并编码,生成查询字符串 # 按照 key 的字母顺序排序 sorted_params = sorted(params.items(), key=lambda x: x[0]) # 将每个参数值进行 URL 编码。注意:只编码 value,不编码 key。 encoded_params = [(k, quote(str(v), safe='')) for k, v in sorted_params] # 拼接成 key=value&... 的格式 query_string = '&'.join([f"{k}={v}" for k, v in encoded_params]) # 2. 按照既定规则拼接明文消息 (message) # 规则: v1|timestamp|path|query_string message = f"v1|{timestamp}|{path}|{query_string}" print(f"[DEBUG] 待签名的明文消息: {message}") # 调试时打印,正式使用可去掉 # 3. 进行 HmacSHA512 计算 # 注意:密钥和消息都需要转换为 bytes 类型 # 通常密钥是字符串,直接 encode() # 如果前端密钥是 Base64 或 Hex,则需要相应解码 key_bytes = secret_key.encode('utf-8') msg_bytes = message.encode('utf-8') # 使用 hmac 库进行计算 hmac_obj = hmac.new(key_bytes, msg_bytes, hashlib.sha512) # 获取十六进制表示的摘要 signature = hmac_obj.hexdigest() print(f"[DEBUG] 生成的签名: {signature}") return signature

关键点解析:

  • quote(str(v), safe=''): 这里使用urllib.parse.quote进行 URL 编码。safe=''参数表示不对任何字符保留(即全部编码),这通常符合前端encodeURIComponent的行为。务必确认前端使用的编码函数,有时可能是encodeURI(不编码/等字符)。
  • sorted(params.items()): 排序是保证拼接顺序一致的关键。前端可能不排序,也可能按其他规则排序,必须完全一致。
  • f”v1|{timestamp}|{path}|{query_string}“: 这个格式模板必须和 JS 代码里的一模一样,包括分隔符|

4.3 构造请求头并发送请求

生成签名后,将其与时间戳一同放入请求头,然后发送请求。

def make_signed_request(url: str, path: str, params: dict, secret_key: str): """ 构造带签名的请求并发送。 """ # 获取当前秒级时间戳 timestamp = int(time.time()) # 生成签名 signature = generate_signature(secret_key, timestamp, path, params) # 构造请求头 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'X-Timestamp': str(timestamp), # 注意:头部的值通常是字符串 'X-Signature': signature, # 可能还需要其他固定或动态的头,如 Content-Type, Referer 等 'Content-Type': 'application/json', } # 发送 GET 请求 (假设是 GET) response = requests.get(url, params=params, headers=headers) # 如果是 POST,可能需要将 params 放到 data 或 json 参数中 # response = requests.post(url, json=params, headers=headers) print(f"请求状态码: {response.status_code}") if response.status_code == 200: print(f"响应数据: {response.json()}") return response.json() else: print(f"请求失败: {response.text}") return None # 使用示例 if __name__ == "__main__": # 替换成你逆向得到的真实密钥 SECRET_KEY = "this_is_my_secret_key_2024" # API 的基础 URL 和路径 BASE_URL = "https://api.example.com" API_PATH = "/api/v4/search" # 请求参数 query_params = { "keyword": "科技有限公司", "page": 1, "size": 20 } full_url = BASE_URL + API_PATH data = make_signed_request(full_url, API_PATH, query_params, SECRET_KEY)

5. 逆向与复现过程中的常见问题与排查技巧

即使逻辑看起来完美复现,在实际操作中依然会遇到各种问题。下面是我踩过的一些坑和对应的排查方法。

5.1 签名验证失败:原因分析与逐项核对

当你的 Python 脚本生成的签名被服务器拒绝时,请按照以下清单逐一核对:

  1. 时间戳不一致

    • 问题:服务器可能要求毫秒级时间戳,而你用了秒级,或者存在时钟漂移。
    • 排查:在浏览器中发起一次成功请求,记录下X-Timestamp头的值。同时用 JavaScriptDate.now()打印一个时间戳。对比两者,确定是秒还是毫秒。在 Python 中,用int(time.time() * 1000)获取毫秒级时间戳。
  2. 消息(Message)拼接不一致(最常见)

    • 问题:这是 90% 失败的原因。多一个空格、少一个竖线、编码不一致、参数顺序不对,都会导致最终的哈希值天差地别。
    • 排查
      • 黄金法则:在 JS 加密函数里,在return signature之前,用console.log(‘message:’, message)console.log(‘signature:’, signature)打印出明文和签名。
      • 在你的 Python 代码的generate_signature函数里,也打印出拼接好的message
      • 将两者进行逐字符对比。可以写一个小函数,或者直接复制到文本比较工具里。确保完全一致。
      • 特别注意中文字符的编码。JS 的encodeURIComponent(‘测试’)会得到%E6%B5%8B%E8%AF%95,Python 的quote(‘测试’)默认也是这个结果,但要确认safe参数设置是否正确。
  3. 密钥(Key)错误

    • 问题:你以为的密钥可能不是最终用于计算的密钥。它可能被 Base64 解码过、被 Hex 解码过,或者与其他字符串拼接过。
    • 排查:在 JS 调试器中,在计算 Hmac 的那一行打上断点,查看传入CryptoJS.HmacSHA512的第二个参数(即密钥)的实际值类型。如果它是CryptoJS.enc.Utf8.parse(“xxx”)的结果,那它就是一个 WordArray 对象,其本质是经过 UTF-8 编码的字节。在 Python 中,我们直接用key.encode(‘utf-8’)即可对应。如果密钥是 Hex 或 Base64 字符串,则需要先用bytes.fromhex()base64.b64decode()处理。
  4. 哈希算法或输出格式错误

    • 问题:虽然说是 HmacSHA512,但有没有可能中间还套了其他处理?比如先 MD5 再 SHA512?或者输出不是 Hex,而是 Base64?
    • 排查:看 JS 代码里,CryptoJS.HmacSHA512(...)之后调用了什么方法。.toString(CryptoJS.enc.Hex)是 Hex,.toString(CryptoJS.enc.Base64)是 Base64。Python 中对应使用.hexdigest().digest()后再base64.b64encode()

5.2 高级技巧:本地 Node.js 验证环境

为了彻底隔离网络和环境变量问题,最高效的方法是在本地 Node.js 环境中,直接运行你从网站“抠”出来的、未经修改的加密函数。

  1. 在浏览器 Sources 面板,找到包含加密函数的整个代码块(可能是一个闭包或模块),将其复制到一个新的.js文件中。
  2. 在这个文件末尾,补上调用代码,并打印结果。例如:
    // 这是从网站复制过来的、包含 generateSignature 函数的代码... // ... // 这是你添加的测试代码 var testTimestamp = 1712345678; var testPath = "/api/v4/search"; var testParams = {keyword: "测试", page: 1}; // 注意:这里需要模拟出原网站获取密钥的环境,比如 window._globalConfig // 如果密钥是写死的,就直接赋值 window = {}; // 简单补环境 window._globalConfig = { apiSecret: 'this_is_my_secret_key_2024' }; var result = generateSignature(testTimestamp, testPath, testParams); console.log('JS 计算结果:', result);
  3. 在终端运行node your_test_file.js,得到签名 A。
  4. 用你的 Python 脚本,使用相同的输入参数,计算得到签名 B。
  5. 对比 A 和 B。如果一致,恭喜你,Python 复现成功。如果不一致,问题一定出在密钥、消息拼接或编码的细节上,而算法本身没问题。你可以修改 JS 测试文件,多打印一些中间变量,与 Python 的中间变量对比。

5.3 应对代码更新与反爬升级

网站的反爬策略不是一成不变的。

  • 密钥轮换:密钥可能会定期更换。如果你的脚本某天突然全部失效,而消息逻辑没错,首先要怀疑密钥变了。需要重新抓包、逆向,定位新的密钥来源。
  • 算法升级:可能从 HmacSHA512 升级到更复杂的算法,或者加入随机盐(salt)、动态密钥。
  • 代码混淆加强:函数名、变量名可能每次更新都变,但核心逻辑通常不变。此时,通过 Hook 请求头设置函数来定位入口点的方法依然有效。
  • 风控拦截:即使签名正确,频繁的、带有异常特征的请求(如无头浏览器、固定 IP 高频访问)也可能被风控系统拦截。需要合理设置请求间隔、使用代理 IP 池、模拟更真实的浏览器指纹(如User-AgentAccept-Language等头信息)。

逆向工程是一场持久战,核心在于理解原理、耐心调试和严谨对比。每一次成功的解密,不仅是为了拿到数据,更是对前端安全机制和密码学应用的一次深刻理解。把每次遇到的坑和解决方案记录下来,形成自己的知识库,以后再遇到类似的加密,解决起来就会快得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询