前言
主流电商、资讯接口通过前端 JS 对请求参数 AES 加密、RSA 密钥加密、MD5 + 随机盐生成 sign 签名,请求参数全密文传输,直接拼接明文参数请求返回验签失败。本章拆解前端 JS 逆向定位加密函数、抠取 JS 加密逻辑、Python 复刻加密算法,实现和前端一致的请求密文,适配 requests 直接调用加密接口,对接现有代理、UA、Cookie 整套爬虫工程。
本文所需依赖官方文档超链接:
- PyCryptodome 官方文档
- Requests 官方文档
- ExecJS 官方文档
一、接口签名加密分类与风控原理
1.1 三种主流加密方案
- AES 对称加密:固定密钥,前端后端共用同一密钥,参数整体 AES-CBC/ECB 加密;
- RSA 非对称加密:公钥前端加密参数,私钥后端解密,密钥拆分无法单靠密文反推原文;
- MD5-SIGN 签名:明文 + 固定盐值拼接后 MD5 哈希生成 sign,参数 + sign 一同提交,后端重复验签。
1.2 爬虫失效原因
直接构造明文参数提交,后端通过 sign / 密文校验判定非法请求,返回 403 / 数据为空。
二、环境依赖安装
bash
运行
pip install requests==2.31.0 pycryptodome==3.20.0 execjs==2.1.0三、逆向排查步骤(浏览器抓包定位加密位置)
- F12 调试→Network 筛选 XHR/Fetch 接口,查看 Payload 密文参数;
- 右键调用堆栈 (Call stack) 定位加密 JS 函数;
- 断点调试,找到加密入口函数、密钥、偏移量、加盐字符串;
- 方案 1:抠 JS 代码用 execjs 直接调用;方案 2:Python 原生密码库复刻算法。
四、方案一:ExecJS 直接调用原生前端 JS(最简逆向)
4.1 提取前端加密 JS 代码示例(模拟前端 AES+sign)
javascript
运行
// encrypt.js const CryptoJS = require('crypto-js'); const KEY = "1234567812345678"; const SALT = "abc123xyz"; function aesEncrypt(data){ return CryptoJS.AES.encrypt(JSON.stringify(data), KEY, {mode:CryptoJS.mode.ECB,padding:CryptoJS.pad.Pkcs7}).toString() } function getSign(param){ let str = JSON.stringify(param)+SALT; return CryptoJS.MD5(str).toString() }4.2 Python 调用 JS 实现参数加密
python
运行
import execjs import requests # 读取本地JS文件 with open("encrypt.js","r",encoding="utf-8") as f: js_code = f.read() ctx = execjs.compile(js_code) def get_encrypt_params(raw_dict): """明文字典传入JS,返回密文+sign""" encrypt_data = ctx.call("aesEncrypt",raw_dict) sign = ctx.call("getSign",raw_dict) return {"data":encrypt_data,"sign":sign} # 接口请求示例 HEADERS = {"User-Agent":"Mozilla/5.0 Chrome/124.0.0.0 Safari/537.36"} def api_crawl(url,plain_params): req_data = get_encrypt_params(plain_params) resp = requests.post(url,json=req_data,headers=HEADERS) return resp.json()五、方案二:Python 原生 AES-ECB 算法复刻(脱离 JS 依赖)
python
运行
from Crypto.Cipher import AES from Crypto.Util.Padding import pad import json import hashlib AES_KEY = b"1234567812345678" SALT_STR = "abc123xyz" def aes_ecb_encrypt(raw_data:dict): """AES-ECB Pkcs7加密""" raw_str = json.dumps(raw_data,ensure_ascii=False).encode("utf-8") cipher = AES(AES_KEY,AES.MODE_ECB) pad_data = pad(raw_str,AES.block_size,style="pkcs7") encrypt_bytes = cipher.encrypt(pad_data) import base64 return base64.b64encode(encrypt_bytes).decode("utf-8") def md5_sign(raw_data:dict): """MD5加盐生成签名""" concat_str = json.dumps(raw_data,ensure_ascii=False)+SALT_STR md5_obj = hashlib.md5(concat_str.encode("utf-8")) return md5_obj.hexdigest() def build_req_body(plain): return {"data":aes_ecb_encrypt(plain),"sign":md5_sign(plain)}六、拓展:RSA 前端公钥加密参数实现
python
运行
from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 import base64 # 前端扒取公钥字符串 RSA_PUB_KEY = """-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDxxx -----END PUBLIC KEY-----""" def rsa_encrypt(content:str): pub_key = RSA.import_key(RSA_PUB_KEY) cipher = PKCS1_v1_5.new(pub_key) encrypt_data = cipher.encrypt(content.encode("utf-8")) return base64.b64encode(encrypt_data).decode()七、动态密钥场景:密钥从接口实时获取
部分站点密钥通过前置接口动态下发,每次请求密钥变更:
python
运行
def get_dynamic_key(): key_resp = requests.get("https://xxx.com/api/getkey",headers=HEADERS) return key_resp.json()["key"] def dynamic_aes_crawl(plain_dict): key = get_dynamic_key().encode("utf-8") cipher = AES(key,AES.MODE_ECB) # 后续加密逻辑复用上方AES代码八、结合资源池完整爬虫示例(UA + 代理 + 加密接口三合一)
python
运行
import requests PROXY_API = "http://127.0.0.1:5010/get_proxy" def encrypt_full_crawl(target_api,plain_param): # 获取代理 proxy_info = requests.get(PROXY_API).json() proxies = {"http":f"http://{proxy_info['proxy']}","https":f"http://{proxy_info['proxy']}"} # 构造加密请求体 body = build_req_body(plain_param) headers = get_random_headers() res = requests.post(target_api,json=body,headers=headers,proxies=proxies,timeout=8) return res.json()九、常见逆向踩坑与优化表
表格
| 问题 | 解决方案 |
|---|---|
| execjs 缺少 node 环境 | 安装 Node.js,或改用 Python 原生密码库复刻 |
| AES 加密结果和前端不一致 | 核对模式 ECB/CBC、偏移 iv、填充格式 Pkcs7/Zero |
| sign 每次变化 | 确认随机时间戳、nonce 字段参与签名拼接 |
| RSA 超长参数加密 | 前端分段 RSA 加密,Python 同步分段处理 |
| 密钥 JS 混淆无法提取 | AST 解混淆、在线 JS 格式化还原源码 |
十、Playwright 绕过加密懒人方案
不想逆向 JS 时,直接在浏览器环境调用页面原生加密函数:
python
运行
from playwright.sync_api import sync_playwright def playwright_get_encrypt_param(url,plain): with sync_playwright() as pw: browser = pw.chromium.launch(headless=True) page = browser.new_page() page.goto(url) # 调用页面全局加密函数 encrypt_res = page.evaluate("(arg)=>{return {data:aesEncrypt(arg),sign:getSign(arg)}}",plain) browser.close() return encrypt_res