1. 项目概述:当爬虫遇到加密的时间戳
最近在做一个数据采集项目时,遇到了一个典型的“拦路虎”:目标网站的接口参数里,有一个看似普通的timestamp字段,但直接使用当前时间戳请求,返回的永远是“签名错误”或“请求已过期”。这让我意识到,这个时间戳被加密了。对于很多刚接触爬虫的朋友来说,遇到这种参数加密,尤其是时间戳这种动态变化的参数,往往会感到无从下手。其实,这恰恰是反爬策略从“简单屏蔽”升级到“动态验证”的标志,也是爬虫工程师从入门到进阶必须跨过的一道坎。
这个项目,我们就来深入实战,拆解如何逆向分析并破解这种接口参数中的时间戳加密。我们将以 Python 作为主要工具,但核心思路是通用的,适用于任何语言。整个过程不仅仅是找到加密算法,更重要的是理解前端 JavaScript 的执行逻辑、如何定位关键代码、以及如何用 Python 完美复现这个动态生成过程。最终,我们的目标是获得一个能够稳定、正确生成加密后时间戳参数的 Python 函数,让爬虫请求畅通无阻。
2. 逆向分析的核心思路与工具准备
逆向分析前端加密,听起来很高深,其实核心思路就是“扮演浏览器”。浏览器能成功请求,是因为它执行了完整的 JavaScript 代码,生成了正确的参数。我们的任务,就是弄清楚它到底执行了哪段代码,以及这段代码的逻辑是什么。
2.1 逆向分析的基本逻辑链条
面对一个加密参数,我们的分析路径应该是清晰的:
- 定位参数:通过浏览器开发者工具(F12)的 Network 面板,找到目标请求,查看其请求参数(Payload),确认哪个是疑似被加密的时间戳字段(通常叫
t,timestamp,_t,sign等)。 - 搜索关键代码:在 Sources 面板中,全局搜索这个加密后的参数字符串,或者搜索参数名(如
timestamp)。如果加密逻辑是通用的,可能会被封装在某个js文件里,通过函数名(如getSign,encryptTimestamp)也能搜到。 - 打断点与跟踪:在疑似生成该参数的代码行设置断点,重新触发请求。当代码执行到断点时,程序会暂停,此时我们可以查看调用栈(Call Stack)、观察局部变量的值、单步执行(F10/F11),一步步跟踪参数是如何从原始时间戳变成最终密文的。
- 逻辑提取与复现:理清加密逻辑后,用 Python 代码将其复现出来。这可能涉及哈希算法(如 MD5, SHA-1, SHA-256)、对称加密(如 AES)、非对称加密(如 RSA)或自定义的位运算、字符串拼接等。
注意:整个过程需要耐心。前端代码可能被混淆(Obfuscated),变量名变成
a, b, c,逻辑被拆散。这时需要依靠对常见加密模式的熟悉和细致的调试来理清头绪。
2.2 必备工具与环境搭建
工欲善其事,必先利其器。以下是本次实战需要用到的核心工具:
- 浏览器开发者工具:Chrome 或 Edge 的 DevTools 是主力。重点关注Network(网络请求监控)、Sources(源码查看与调试)、Console(控制台执行 JS) 这三个面板。
- Node.js 环境:有些加密库是 Node.js 特有的,或者在浏览器环境中难以直接调试。安装 Node.js 后,我们可以将关键的 JS 函数片段提取出来,在本地用 Node.js 运行和测试,验证我们的理解是否正确。这对于复现复杂加密逻辑至关重要。
- Python 环境及库:确保你的 Python 环境已安装以下库:
requests: 用于发送 HTTP 请求。execjs: 一个非常关键的库,它允许 Python 执行 JavaScript 代码。当加密逻辑过于复杂,用纯 Python 复现困难或存在细微差异时,可以直接调用原始的 JS 代码。hashlib,hmac: Python 标准库,用于实现常见的哈希算法。Crypto(或pycryptodome): 如果需要 AES、RSA 等标准加密算法,这个库是必备的。json,time,re等标准库辅助处理数据。
安装命令示例:
pip install requests execjs pycryptodome实操心得:在开始逆向之前,先用浏览器正常手动操作一遍,确保你能成功触发一次目标请求。同时,留意是否有设置Cookie、User-Agent或特定的Header。这些信息在后续用 Python 模拟请求时同样重要,缺失它们也可能导致请求失败。
3. 实战案例拆解:定位并分析时间戳加密逻辑
我们以一个虚构但非常典型的场景为例:目标网站api.example.com/data的 POST 请求,需要携带参数timestamp和sign。其中timestamp看起来是加密后的字符串,而非直接的 Unix 时间戳。
3.1 网络抓包与初步观察
打开浏览器开发者工具的 Network 面板,勾选Preserve log(保留日志),然后进行触发数据加载的操作。找到目标请求api.example.com/data,点击查看Headers和Payload。
- Request Headers:注意是否有
Authorization,X-Requested-With等特殊头。 - Request Payload:我们可能看到类似这样的数据:
这里的{ "page": 1, "size": 20, "timestamp": "a1b2c3d4e5f67890abcdef1234567890", "sign": "f0e1d2c3b4a59687fedcba9876543210" }timestamp是一串十六进制字符串,长度固定为32位,这强烈暗示它可能是一个MD5或SHA-1(40位)哈希值,或者是 AES 加密后的结果。而sign字段通常是所有参数(包括加密后的 timestamp)按照一定规则拼接后,再进行一次哈希运算得到的签名,用于防止参数被篡改。
3.2 搜索与定位加密函数
在 Sources 面板中,按Ctrl+Shift+F进行全局搜索。我们可以尝试搜索:
- 直接搜索加密后的
timestamp值"a1b2c3d4e5f67890abcdef1234567890"。如果这个值是在前端生成的,有很大概率能直接定位到生成它的代码行。 - 搜索参数名
"timestamp",查看哪些 JS 文件包含对这个字段的赋值或处理逻辑。 - 搜索常见的加密函数名或关键词,如
encrypt,MD5,SHA1,createHash,CryptoJS,getTimestamp,sign。
假设我们搜索"timestamp",在一个名为app.xxxxxx.js的混淆文件中找到了如下代码片段:
function getEncryptedTimestamp() { var t = (new Date).getTime(); var e = encryptFunc(t.toString()); return e }这是一个非常清晰的线索!它表明,加密后的timestamp是由当前时间戳t经过一个叫encryptFunc的函数处理得到的。接下来,我们的目标就是找到encryptFunc的定义。
3.3 深入调试与逻辑还原
在var e = encryptFunc(t.toString());这一行点击行号设置断点。然后刷新页面或重新触发请求。当代码执行到断点时,程序会暂停。
- 查看变量值:在右侧的 Scope 面板中,可以看到此时变量
t的值,例如1715167890123。这是一个标准的13位毫秒级 Unix 时间戳。 - 跟踪函数:将鼠标悬停在
encryptFunc上,可能会显示其定义,或者我们可以点击 Step into (F11) 进入这个函数内部。 - 分析 encryptFunc:进入后,我们可能看到类似这样的代码:
太好了!逻辑非常清晰:将字符串化的时间戳,用固定的密钥(key)和初始向量(iv),通过AES-128-CBC模式加密,并将结果转换为十六进制字符串输出。function encryptFunc(input) { var key = "a_secret_key_123"; var iv = "initial_vector_456"; // 使用 CryptoJS 进行 AES-128-CBC 加密 var encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(input), CryptoJS.enc.Utf8.parse(key), { iv: CryptoJS.enc.Utf8.parse(iv), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.ciphertext.toString(CryptoJS.enc.Hex); }
排查技巧实录:有时,encryptFunc可能被进一步混淆,或者 key/iv 是从其他接口动态获取的。这时需要:
- 查看调用栈:在 Call Stack 面板中,查看
encryptFunc是被谁调用的,向上回溯可能找到 key/iv 的生成或获取逻辑。 - 搜索常量:在 Sources 中搜索
"a_secret_key"或"CryptoJS.AES.encrypt"来定位更多相关代码。 - 控制台测试:在 Console 面板中,我们可以直接复制
encryptFunc函数和相关依赖(如 CryptoJS 对象),然后手动调用encryptFunc("1715167890123"),看输出是否与网络请求中的timestamp值一致。这是验证我们分析是否正确的最快方法。
4. Python 复现加密逻辑的两种策略
分析清楚 JS 逻辑后,就到了用 Python 复现的环节。根据加密复杂程度,我们有两种主要策略。
4.1 策略一:纯 Python 复现(推荐)
如果加密算法是标准的,如 MD5、SHA-256、AES、RSA 等,优先使用 Python 的密码学库进行复现。这样做性能好,依赖少,也更“原生”。
对于上面分析的 AES-128-CBC 加密,Python 复现代码如下:
import time from Crypto.Cipher import AES from Crypto.Util.Padding import pad import binascii def encrypt_timestamp_pure_python(): """ 使用纯Python复现AES-128-CBC加密时间戳 """ # 1. 获取当前13位毫秒时间戳 raw_ts = str(int(time.time() * 1000)) # 例如: "1715167890123" # 2. 定义密钥和IV(与JS中完全一致) key = b"a_secret_key_123" # 注意:AES-128密钥长度为16字节 iv = b"initial_vector_456" # IV长度也为16字节 # 3. 创建AES加密器,模式为CBC cipher = AES.new(key, AES.MODE_CBC, iv) # 4. 对明文进行PKCS7填充并加密 # 需要将字符串编码为字节,并进行填充 plaintext_bytes = raw_ts.encode('utf-8') padded_bytes = pad(plaintext_bytes, AES.block_size) encrypted_bytes = cipher.encrypt(padded_bytes) # 5. 将加密后的字节转换为十六进制字符串 encrypted_hex = binascii.hexlify(encrypted_bytes).decode('utf-8') return encrypted_hex # 测试 if __name__ == "__main__": encrypted_ts = encrypt_timestamp_pure_python() print(f"生成的加密时间戳: {encrypted_ts}") # 可以与浏览器抓包得到的值进行对比关键点解析:
- 字节与字符串:Python 的加密库操作的是字节(bytes),而 JS 中通常是字符串。
encode()和decode()是关键。 - 填充模式:AES 是块加密,需要填充到块大小的整数倍。必须确认 JS 使用的填充模式(这里是 PKCS7),并在 Python 中使用对应的填充函数(
pad)。 - 密钥和 IV:必须确保与 JS 中的值完全一致,包括长度。AES-128 的 key 是 16 字节。
4.2 策略二:使用 execjs 调用 JavaScript 代码
当遇到非常复杂、自定义、或者严重依赖浏览器环境对象(如window,document)的加密逻辑时,用 Python 完全重写可能事倍功半。此时,execjs库是救星。它允许我们在 Python 中创建一个 JS 执行环境,直接运行那段 JS 代码。
首先,我们需要将关键的 JS 函数提取出来,保存为一个字符串或单独的.js文件。假设我们提取了如下代码到encrypt.js文件:
// encrypt.js const CryptoJS = require("crypto-js"); // 假设我们通过Node.js环境引入 function encryptFunc(input) { var key = "a_secret_key_123"; var iv = "initial_vector_456"; var encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(input), CryptoJS.enc.Utf8.parse(key), { iv: CryptoJS.enc.Utf8.parse(iv), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.ciphertext.toString(CryptoJS.enc.Hex); } // 导出函数供Node.js或execjs使用 module.exports = { encryptFunc };然后,在 Python 中这样调用:
import time import execjs def encrypt_timestamp_via_js(): """ 通过execjs调用JS代码生成加密时间戳 """ # 1. 读取JS文件内容 with open('encrypt.js', 'r', encoding='utf-8') as f: js_code = f.read() # 2. 编译JS代码 ctx = execjs.compile(js_code) # 3. 获取当前时间戳 raw_ts = str(int(time.time() * 1000)) # 4. 调用JS函数 encrypted_ts = ctx.call("encryptFunc", raw_ts) return encrypted_ts # 测试 if __name__ == "__main__": encrypted_ts = encrypt_timestamp_via_js() print(f"通过JS生成的加密时间戳: {encrypted_ts}")实操心得与避坑指南:
- 环境依赖:
execjs背后需要有一个 JS 运行时。在 Windows 上它默认使用 JScript,功能有限。强烈建议安装Node.js,然后通过execjs.get().name检查当前运行时,最好指定为Node.js。 - 模块引入:如果 JS 代码中使用了
require(‘crypto-js’),你需要确保在运行 Python 脚本的机器上,通过npm install crypto-js安装了该库。execjs会继承 Node.js 的环境。 - 性能考虑:频繁调用
execjs创建上下文和执行 JS 会有性能开销。如果爬虫请求频率很高,建议将时间戳的生成批量处理,或者将核心 JS 逻辑用 Python 重写以提升效率。 - 错误处理:JS 代码执行出错时,
execjs会抛出异常。务必用try...except包裹,并打印详细的错误信息,这有助于调试 JS 环境或代码本身的问题。
5. 整合到爬虫请求与签名验证
成功生成加密时间戳后,这通常只是第一步。很多接口还有签名(sign)校验。签名算法通常是将所有请求参数(包括加密后的timestamp)按照特定顺序(如字典序)拼接成一个字符串,然后加上一个私钥(secret),再进行一次哈希运算(如 MD5)。
5.1 生成完整签名示例
假设接口要求参数按字母顺序排序后拼接,然后进行 MD5 签名。
import hashlib import urllib.parse def generate_sign(params, secret): """ 生成请求签名 :param params: dict, 所有请求参数,包括加密后的timestamp :param secret: str, 签名密钥 :return: str, 计算得到的sign值 """ # 1. 对参数按键进行排序 sorted_params = sorted(params.items(), key=lambda x: x[0]) # 2. 拼接成 key1=value1&key2=value2 的格式 query_string = '&'.join([f"{k}={v}" for k, v in sorted_params]) # 3. 在末尾拼接上密钥 sign_string = query_string + f"&secret={secret}" # 4. 计算MD5(注意编码) m = hashlib.md5() m.update(sign_string.encode('utf-8')) return m.hexdigest() # 模拟请求参数 params = { "page": 1, "size": 20, "timestamp": encrypt_timestamp_pure_python(), # 使用之前生成的加密时间戳 } secret_key = "your_api_secret_here" sign = generate_sign(params, secret_key) params['sign'] = sign # 将签名加入请求参数 print(f"最终请求参数: {params}")5.2 发送完整的请求
现在,我们可以使用requests库发送携带正确参数的请求了。
import requests import json url = "https://api.example.com/data" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Content-Type": "application/json", # 根据接口实际情况调整 } # 使用上面生成的 params response = requests.post(url, headers=headers, data=json.dumps(params)) # 或者如果接口接受 form-data,则使用:requests.post(url, headers=headers, data=params) if response.status_code == 200: data = response.json() print("请求成功,数据:", data) else: print(f"请求失败,状态码: {response.status_code}, 响应: {response.text}")6. 常见问题排查与进阶技巧
即使按照步骤操作,在实际复现过程中也可能遇到各种问题。这里记录一些常见的坑和排查思路。
6.1 加密结果不一致问题排查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| Python 和 JS 生成的密文完全不同。 | 1.时间戳源不一致:JS 用的是Date.now(),Python 用的是time.time(),可能存在微小误差。但更大的可能是格式(字符串/数字)或精度(秒/毫秒)问题。2.密钥/IV 不一致:检查字符串是否完全一致,包括不可见字符、编码(UTF-8/ASCII)。 3.加密模式或填充错误:确认是 CBC 还是 ECB?是 PKCS7 还是 ZeroPadding? 4.输入数据格式:JS 中 CryptoJS.enc.Utf8.parse(input)将字符串转为 WordArray,Python 中需要input.encode(‘utf-8’)转为 bytes。 | 1.固定输入:在 JS 调试器中,用一个固定字符串(如”test123″)作为输入,记录输出。在 Python 中用同样的固定字符串输入,对比输出。这样可以排除时间戳动态变化的影响。2.打印中间值:在 JS 和 Python 中,分别打印出密钥、IV、填充前的明文字节,进行逐字节对比。 3.查阅文档:确认 CryptoJS 和 PyCryptodome 库对于相同模式和填充的实现是否完全等价。 |
使用 execjs 调用时报错,提示require未定义或模块找不到。 | 1.运行时环境错误:execjs未使用 Node.js 环境。2.Node.js 模块未安装:JS 代码中引用的 npm 包(如 crypto-js)在系统全局或当前目录下未安装。 | 1. 安装 Node.js,并在 Python 中指定运行时:execjs.get(‘Node’).compile(js_code)。2. 在包含 encrypt.js的目录下,运行npm init -y和npm install crypto-js。或者使用execjs时,将crypto-js的源码直接内联到 JS 代码字符串中,避免require。 |
| 请求返回“签名错误”,但时间戳加密似乎对了。 | 1.签名算法理解有误:参数拼接顺序、是否包含 URL、是否对值进行了 URL 编码等细节出错。 2.密钥错误:用于签名的 secret不对。3.参数遗漏或多余:可能有些隐含参数(如固定的 appId)也需要参与签名,但被遗漏了。 | 1.仔细对比:用浏览器成功请求的原始数据,和你 Python 代码生成的签名字符串进行逐字符对比。 2.网络抓包验证:在开发者工具的 Network 面板中,查看请求的Payload或Query String Parameters,确保你的 Python 代码生成的参数列表和顺序与之完全一致。 3.控制台复现:尝试在浏览器 Console 中,用你的 Python 逻辑(翻译成 JS)计算一遍签名,看是否与请求中的签名一致。 |
6.2 应对代码混淆与动态密钥
更高级的反爬可能会使用以下手段:
- 代码混淆:变量和函数名被压缩成无意义的字符。应对方法是:1) 依靠断点调试,关注输入输出的值,而不是变量名;2) 搜索加密后结果的常量片段;3) 使用浏览器插件或本地工具进行反混淆(可读性提升有限)。
- 动态密钥:加密用的 key 或 iv 可能不是硬编码在 JS 里的,而是通过另一个接口获取,或者由页面上的某些元素值计算得出。这时需要:1) 在调用加密函数前设置断点,查看 key/iv 的值是从哪个变量来的;2) 在 Network 面板中搜索可能返回密钥的 XHR 请求;3) 分析 key/iv 的生成算法,可能也是由时间戳、Cookie 等计算得来,需要一并复现。
6.3 保持爬虫的健壮性与道德规范
- 错误重试与超时:网络请求可能失败,接口可能暂时不可用。为你的请求添加重试机制(如
tenacity库)和合理的超时设置。 - 速率限制:严格遵守网站的
robots.txt协议(如果有),并主动限制请求频率,避免对目标服务器造成过大压力。使用time.sleep()在请求间添加随机间隔。 - User-Agent 轮换:使用一个常见的浏览器 User-Agent 列表进行轮换,避免因单一 UA 被识别。
- 处理 Cookie 和 Session:对于需要登录的网站,使用
requests.Session()来保持会话状态,自动处理 Cookie。 - 明确数据用途:爬取的数据请用于个人学习、分析或符合网站条款的合法用途。尊重数据版权和用户隐私。
破解接口参数加密,尤其是时间戳加密,是一个需要耐心、细心和逻辑分析能力的过程。它没有一成不变的公式,每个网站都可能不同。核心在于掌握“观察-定位-调试-复现”这一套方法论,并熟练运用开发者工具和编程语言进行验证。当你成功绕过一道加密防线时,获得的不仅是数据,更是解决问题的能力和对网络通信更深的理解。