前言主流电商、资讯接口通过前端 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 requests2.31.0 pycryptodome3.20.0 execjs2.1.0三、逆向排查步骤浏览器抓包定位加密位置F12 调试→Network 筛选 XHR/Fetch 接口查看 Payload 密文参数右键调用堆栈 (Call stack) 定位加密 JS 函数断点调试找到加密入口函数、密钥、偏移量、加盐字符串方案 1抠 JS 代码用 execjs 直接调用方案 2Python 原生密码库复刻算法。四、方案一ExecJS 直接调用原生前端 JS最简逆向4.1 提取前端加密 JS 代码示例模拟前端 AESsignjavascript运行// 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,encodingutf-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,jsonreq_data,headersHEADERS) 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 b1234567812345678 SALT_STR abc123xyz def aes_ecb_encrypt(raw_data:dict): AES-ECB Pkcs7加密 raw_str json.dumps(raw_data,ensure_asciiFalse).encode(utf-8) cipher AES(AES_KEY,AES.MODE_ECB) pad_data pad(raw_str,AES.block_size,stylepkcs7) 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_asciiFalse)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,headersHEADERS) 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:fhttp://{proxy_info[proxy]},https:fhttp://{proxy_info[proxy]}} # 构造加密请求体 body build_req_body(plain_param) headers get_random_headers() res requests.post(target_api,jsonbody,headersheaders,proxiesproxies,timeout8) return res.json()九、常见逆向踩坑与优化表表格问题解决方案execjs 缺少 node 环境安装 Node.js或改用 Python 原生密码库复刻AES 加密结果和前端不一致核对模式 ECB/CBC、偏移 iv、填充格式 Pkcs7/Zerosign 每次变化确认随机时间戳、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(headlessTrue) 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