Python逆向复现前端AES加密逻辑,破解政务数据接口实战
1. 项目概述与核心需求解析最近在分析一些地方政务公开数据时遇到了一个典型的案例湖南省农机购置与应用补贴信息的查询接口。这个接口的数据对分析地方农业政策落地、农机市场趋势很有价值但和很多政务数据接口一样它并非“裸奔”而是对请求参数和响应数据做了简单的加密处理。项目标题里提到“难度一般扣代码即可无需补环境”这基本定调了——这不是一个需要复杂逆向、模拟浏览器环境或处理高强度混淆的硬骨头而是一个典型的“逻辑加密”场景。我们的目标很明确用 Python 这把“螺丝刀”把前端 JavaScript 中的加密逻辑“抠”出来在本地复现从而能稳定、自动化地获取到解密后的 JSON 数据。为什么这类接口值得一做首先数据源稳定且权威来自官方平台数据质量有保障。其次加密方式相对固定一旦破解可以长期使用。最后这类数据在农业经济分析、区域产业研究甚至商业情报收集方面都有实际应用场景。比如你可以分析哪些型号的拖拉机在湖南最受欢迎补贴资金流向哪些县市从而洞察市场的真实需求。整个过程我们完全在本地 Python 环境中完成不依赖浏览器自动化效率高、资源消耗小非常适合作为爬虫工程师处理常见数据加密的入门到进阶的练手项目。2. 逆向分析与加密逻辑定位面对一个加密接口第一步永远是观察。打开浏览器开发者工具F12切换到 Network网络标签页找到目标请求。通常这类补贴查询是一个POST请求请求体Payload不是明文的form-data或x-www-form-urlencoded而是一串看起来像乱码的字符串或者是一个结构体里包含了data、encryptData、sign等字段。响应体Response同样可能是一串加密后的字符串而非直观的 JSON。2.1 关键线索搜索与断点前端加密逻辑必然存在于某个加载的 JavaScript 文件中。我们的核心策略是“搜索关键词”和“下断点”。搜索加密相关关键词在开发者工具的 Sources源代码标签页中对所有 JS 文件进行全局搜索。关键词可以包括接口 URL 中的部分路径。请求参数中明显的字段名如data,encryptStr,param。常见的加密算法名如AES,DES,RSA,SM2,SM4,encrypt,decrypt。可能用于生成签名sign的MD5、SHA256或SM3。在这个案例中通过搜索encrypt或接口关键参数我们很可能定位到一个名为xxx.encrypt.js或包含crypto、security字样的文件或者加密逻辑就写在一个主业务 JS 文件里。在请求发起处下 XHR/Fetch 断点在 Network 标签页找到目标请求右键选择 “Copy - Copy as fetch”。然后在 Sources 标签页的 “XHR/fetch Breakpoints” 里添加一个包含该接口 URL 关键词的断点。重新触发请求代码执行会自动暂停在发起网络请求的那一行 JavaScript 代码处。从这里开始单步执行F11或查看调用栈Call Stack一步步回溯就能找到参数被加密处理的具体函数。2.2 核心逻辑剖析跟进去之后你会发现加密逻辑通常不复杂。常见套路如下对称加密如 AES前端使用一个固定的密钥Key和初始向量IV对拼接好的参数字符串进行 AES 加密输出可能是 Base64 或 Hex 格式的字符串。这个密钥和 IV 很可能硬编码在 JS 文件里。非对称加密如 RSA前端用后端提供的公钥Public Key对某个关键信息如随机生成的 AES 密钥进行加密。但更多时候RSA 用于加密一个临时生成的对称密钥。哈希/签名如 MD5, SHA256, SM3将参数按特定规则拼接后进行哈希计算生成一个签名sign用于防止参数被篡改。这个sign会作为另一个参数一同发送。自定义编码/混淆有时并非标准加密算法而是简单的字符替换、顺序打乱或结合时间戳的运算。注意在扣代码时重点不是理解算法深奥的数学原理而是准确地复现流程。你需要记录下输入是什么原始参数对象经过了哪些函数处理函数名、参数顺序调用了哪些库或原生方法最终输出是什么。用浏览器的控制台Console在断点处实时执行代码、打印中间变量值是最高效的验证方法。3. 加密逻辑的 Python 复现将 JavaScript 加密逻辑“翻译”成 Python 是核心环节。这里假设我们分析出的加密方式是AES-128-CBC模式密钥和 IV 固定输出为 Base64。3.1 环境准备与库选择首先确保你的 Python 环境安装了必要的库。最常用的是pycryptodome它是PyCrypto的一个维护良好的分支。pip install pycryptodome如果涉及到国密算法如 SM2, SM3, SM4则需要安装gmssl库。pip install gmssl3.2 JavaScript 逻辑与 Python 代码对比假设我们在 JS 中找到了如下加密函数示例// 伪代码基于常见写法 function encryptData(data) { const key CryptoJS.enc.Utf8.parse(1234567890123456); // 16字节密钥 const iv CryptoJS.enc.Utf8.parse(abcdefghijklmnop); // 16字节IV const srcs CryptoJS.enc.Utf8.parse(JSON.stringify(data)); const encrypted CryptoJS.AES.encrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); // 默认返回 Base64 字符串 }对应的 Python 复现代码如下import json from base64 import b64encode from Crypto.Cipher import AES from Crypto.Util.Padding import pad def encrypt_data_py(data_dict): 复现前端的 AES-128-CBC 加密逻辑 # 1. 准备密钥和IV必须确保是字节类型bytes key b1234567890123456 # 16字节 iv babcdefghijklmnop # 16字节 # 2. 将数据字典转换为JSON字符串再编码为字节 json_str json.dumps(data_dict, separators(,, :), ensure_asciiFalse) # 注意ensure_asciiFalse 允许中文但最终加密的是其utf-8字节。有些前端库可能默认 ascii。 # 稳妥起见可以对比前端加密前字符串的字节。这里先按 False 处理。 data_bytes json_str.encode(utf-8) # 3. 创建 AES 加密器使用 CBC 模式和 PKCS7 填充 cipher AES.new(key, AES.MODE_CBC, iv) # 4. 对数据进行填充并加密 # PKCS7 填充缺 N 个字节就填充 N 个值为 N 的字节 padded_data pad(data_bytes, AES.block_size) encrypted_bytes cipher.encrypt(padded_data) # 5. 将加密后的字节进行 Base64 编码 encrypted_b64 b64encode(encrypted_bytes).decode(utf-8) return encrypted_b64 # 测试 if __name__ __main__: test_data { pageNum: 1, pageSize: 10, cityCode: 430000 } result encrypt_data_py(test_data) print(加密结果:, result) # 可以与浏览器控制台执行原JS函数的结果对比必须完全一致3.3 关键细节与避坑指南编码一致性这是最大的坑。JavaScript 的CryptoJS.enc.Utf8.parse和 Python 的.encode(‘utf-8’)在绝大多数情况下是一致的。但遇到特殊字符或前端做了额外处理时可能会不同。务必在浏览器控制台打印出加密前的原始字符串JSON.stringify(data)的结果和对应的字节数组与 Python 中生成的字符串和字节数组进行逐位对比。填充模式PKCS7填充是常见的。Python 的pycryptodome库中pad函数默认使用PKCS7。确保与前端配置CryptoJS.pad.Pkcs7一致。输出格式前端encrypted.toString()默认输出 Base64 字符串。确认是否是 Hex 格式如果是Python 侧就用.hex()方法。JSON 序列化json.dumps时使用separators(‘,’, ‘:’)可以移除空格生成最紧凑的 JSON这与JSON.stringify的默认行为更接近。ensure_ascii参数需要根据实际情况调整。密钥和 IV 长度AES-128 对应 16 字节密钥AES-256 对应 32 字节。IV 长度通常与块大小一致16字节。实操心得不要相信“看起来一样”。一定要做单元测试。将完全相同的输入一个字典分别交给浏览器控制台里的 JS 函数和你的 Python 函数比较输出是否一字不差。这是验证扣代码成功与否的唯一标准。4. 解密响应数据很多情况下服务器返回的数据也是加密的。解密过程是加密的逆过程前提是你已经知道了算法和密钥通常与请求加密相同或可以从首次响应、其他接口中推导出来。4.1 Python 解密实现继续上面的 AES 例子假设响应体是一个 Base64 字符串我们需要解密它得到 JSON。from base64 import b64decode from Crypto.Cipher import AES from Crypto.Util.Padding import unpad def decrypt_response_py(encrypted_b64_str): 解密服务器返回的 AES-128-CBC 加密数据 # 1. 使用相同的密钥和IV key b1234567890123456 iv babcdefghijklmnop # 2. 将 Base64 字符串解码为字节 encrypted_bytes b64decode(encrypted_b64_str) # 3. 创建 AES 解密器 cipher AES.new(key, AES.MODE_CBC, iv) # 4. 解密并去除填充 decrypted_padded_bytes cipher.decrypt(encrypted_bytes) # 注意解密后先不要解码为字符串因为末尾有填充字节 decrypted_bytes unpad(decrypted_padded_bytes, AES.block_size) # 5. 将字节解码为 UTF-8 字符串再解析为 JSON json_str decrypted_bytes.decode(utf-8) data_dict json.loads(json_str) return data_dict # 假设从网络请求获取的加密响应文本是 resp_encrypted_text # decrypted_data decrypt_response_py(resp_encrypted_text)4.2 处理可能的变化响应结构有时响应体是一个 JSON 对象其中某个字段如data或result才是加密的字符串。你需要先解析外层 JSON取出加密字段再进行解密。动态密钥更复杂的情况是密钥或 IV 不是固定的可能由第一个接口响应提供或者由前端根据时间戳等因子计算得出。这就需要你完整跟踪前端初始化或登录时的密钥协商流程。算法组合例如先用 RSA 加密一个随机生成的 AES 密钥再用这个 AES 密钥加密数据。这就需要你同时复现 RSA 和 AES 两种逻辑。5. 构建完整的请求流程与参数处理有了加密解密函数我们就可以组装一个完整的、可用的爬虫脚本了。5.1 请求参数组装分析前端页面找出查询所需的全部参数。这些参数可能包括分页信息pageNum,pageSize、查询条件如城市代码cityCode、农机类型machineType、时间范围等。将这些参数组装成一个 Python 字典。5.2 签名Sign的生成如果接口除了加密参数data外还需要一个签名sign那么你需要找到生成签名的规则。常见规则是将所有参数或部分参数按参数名排序后拼接成key1value1key2value2...的形式然后加上一个固定的secret盐值最后对这个字符串进行MD5或SM3哈希。import hashlib def generate_sign(params_dict, secretyour_secret_key): 生成 MD5 签名 # 1. 参数排序并拼接 sorted_params sorted(params_dict.items(), keylambda x: x[0]) param_str .join([f{k}{v} for k, v in sorted_params]) # 2. 拼接密钥 sign_str param_str secret # 3. 计算 MD532位小写 md5 hashlib.md5() md5.update(sign_str.encode(utf-8)) return md5.hexdigest() # 注意有时 secret 可能直接拼接在字符串末尾有时是 keyvaluesecretxxx 格式具体看前端逻辑。5.3 发送请求与处理响应使用requests库发送 POST 请求。请求头Headers需要模拟至少包含Content-Type: application/json通常还需要User-Agent和一些特定的 Referer 或 Origin。import requests def fetch_subsidy_data(query_params): 获取补贴数据的主函数 url https://xxx.hunan.gov.cn/xxx/query # 替换为实际接口地址 # 1. 加密请求参数 encrypted_data encrypt_data_py(query_params) # 2. 生成签名如果需要 # 注意签名计算的参数可能是原始参数也可能是包含加密后数据的参数需根据前端确定 sign_params {data: encrypted_data, timestamp: int(time.time()*1000)} sign generate_sign(sign_params) # 3. 组装最终请求体 payload { data: encrypted_data, sign: sign, # ... 可能还有其他固定字段 } # 4. 设置请求头 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..., Content-Type: application/json;charsetUTF-8, Referer: https://xxx.hunan.gov.cn/, # 通常需要 Origin: https://xxx.hunan.gov.cn, } # 5. 发送请求 try: response requests.post(url, jsonpayload, headersheaders, timeout10) response.raise_for_status() # 检查HTTP错误 # 6. 处理响应 resp_json response.json() # 情况A响应整体是加密字符串 if isinstance(resp_json, str): decrypted_data decrypt_response_py(resp_json) return decrypted_data # 情况B响应是JSON其中某个字段是加密字符串 elif isinstance(resp_json, dict) and data in resp_json: encrypted_resp_data resp_json[data] decrypted_data decrypt_response_py(encrypted_resp_data) return decrypted_data else: print(响应格式未知:, resp_json) return None except requests.exceptions.RequestException as e: print(f网络请求失败: {e}) return None except json.JSONDecodeError as e: print(f响应JSON解析失败: {e}, 原始文本: {response.text[:200]}) return None except Exception as e: print(f解密或处理过程出错: {e}) return None6. 常见问题排查与实战技巧即使逻辑扣对了在实际运行中也可能遇到各种问题。下面是一个常见问题速查表。问题现象可能原因排查思路与解决方案加密结果与前端不一致1. 密钥/IV错误或编码不对。2. 加密前的源字符串不一致JSON格式、空格、中文编码。3. 加密模式或填充模式错误。1.逐字节对比在浏览器控制台和Python中分别打印出加密前字符串的charCodeAt/bytes和加密后的结果。这是最有效的调试方法。2.检查JSON使用JSON.stringify(data, null, 0)在前端生成最紧凑JSON与Python的json.dumps(data, separators(‘,’,‘:’), ensure_asciiFalse)结果对比。3.验证算法确认前端使用的库和具体算法名如AES-128-CBC-Pkcs7。请求成功但返回“签名错误”或“参数错误”1. 签名生成规则有误参数顺序、拼接方式、secret值。2. 用于签名的参数集合不对可能漏了某些固定参数或时间戳。3. 加密后的data字段在传输中被意外处理如二次URL编码。1.网络抓包对比用工具如 Charles/Fiddler拦截浏览器正常请求完整对比其Payload和你代码生成的Payload的每一个字段。2.逆向签名函数在JS中给签名生成函数下断点记录下参与签名的完整字符串与Python生成的字符串逐字对比。3.检查请求头Content-Type是否正确有些接口要求application/x-www-form-urlencoded而不是application/json。返回的数据解密失败1. 解密用的密钥/IV与加密不同。2. 响应数据不是纯粹的加密字符串可能包含前缀、后缀或进行了包装。3. 解密后的数据填充错误unpad失败。1.检查响应结构先打印response.json()或response.text看清返回的到底是什么结构。2.尝试直接解密如果响应是{“code”:0,“data”:”加密字符串”}那么你需要解密的是data字段的值。3.错误处理在decrypt和unpad处做好try...except捕获异常并打印中间变量有助于定位。请求被拒绝返回403或4121. 缺少必要的请求头如Referer,Origin,X-Requested-With或特定的自定义头。2. 存在反爬机制如需要Cookie中的令牌Token或验证码。1.完整复制Headers使用浏览器开发者工具将成功请求的所有Headers键值对复制到你的代码中特别是Cookie和User-Agent。2.处理会话使用requests.Session()对象来保持Cookie。3.模拟完整流程可能需要先访问首页获取初始Cookie甚至模拟登录来获取有效的认证Token。代码在本地运行正常但部署后偶尔失败1. 服务器端密钥或算法有变动较少见。2. 网络环境或代理问题。3. 时间戳不同步导致签名过期。1.增加日志记录每次请求的明文参数、加密结果、签名和完整URL出问题时方便对比分析。2.重试机制对于网络超时等临时错误加入指数退避的重试逻辑。3.验证时间戳如果签名依赖时间戳确保本地时间与网络时间同步。6.1 独家避坑技巧“抠”代码而不是“猜”代码永远以浏览器实际执行的代码为基准。即使你看到 JS 里有个变量叫key也不要直接用它而要打印出它在运行时的实际值。因为变量可能被其他代码修改。善用控制台在浏览器开发者工具的 Console 里你可以直接调用找到的加密函数传入测试参数立即看到结果。这是验证逻辑最快的方式。最小化测试不要一开始就处理完整的复杂参数。构造一个最简单的参数字典如{“test”: 1}分别用 JS 和 Python 加密先让这个最小案例跑通。关注非加密参数像pageNum,pageSize,timestamp,nonce随机数这类参数虽然不加密但却是请求的必要组成部分且timestamp和nonce可能用于防重放需要按规则生成。代码健壮性在decrypt和json.loads等可能出错的地方做好异常捕获和日志记录。对于重要爬虫可以考虑将成功解密的原始响应和解析后的数据都保存下来便于后续排查和数据分析。这个项目虽然被标注为“难度一般”但它涵盖了爬虫工程师处理加密接口的完整工作流抓包观察、逆向定位、逻辑分析、代码复现、请求构建、异常处理。成功搞定之后你收获的不仅仅是一份数据更是一套可复用于其他类似“逻辑加密”接口的方法论。下次再遇到{“data”: “xxx”}这种格式的请求你就能从容地打开开发者工具开始你的“抠代码”之旅了。