逆向AES加密接口与动态Token获取:Python爬虫实战解析
1. 项目概述与核心挑战最近在做一个数据采集相关的项目目标是一个提供全国建筑市场信息的网站。这类网站通常包含企业资质、人员信息、项目业绩等关键数据对于行业分析、市场调研来说价值很高。但和很多现代网站一样它采用了AES加密来保护接口返回的数据并且访问接口需要一个动态的accesstoken。这听起来像是一个典型的“前端加密、后端解密”的反爬策略组合拳。如果你直接上手用requests库去请求接口拿回来的很可能是一堆你看不懂的乱码或者干脆就是加密后的密文字符串。这个项目的核心就是如何像浏览器一样先拿到合法的通行证accesstoken再破解数据保险箱AES解密最终拿到结构化的明文信息。这不仅仅是写几行爬虫代码那么简单它涉及到对网站登录认证流程的逆向、对前端JavaScript加密逻辑的分析以及对AES加密算法在Web场景下具体应用模式的理解。整个过程更像是一次小型的“安全审计”你需要暂时站在网站开发者的角度去理解他们是如何设计这套保护机制的然后再以数据采集者的身份找到合规且稳定的方式绕过它。我之所以说“合规”是因为我们的所有操作都基于公开的接口和前端加载的代码不涉及破解服务器、不进行暴力请求核心思路是模拟合法用户的行为。下面我就把这次实战中趟过的路、踩过的坑以及最终稳定运行的方案完整地分享给你。2. 逆向工程定位accesstoken与加密逻辑动手写代码之前最关键的一步是“侦察”。我们需要弄清楚两件事第一accesstoken从哪里来如何获取第二数据返回后是用哪种模式的AES加密的密钥和初始向量IV又藏在哪里。2.1 网络请求追踪与accesstoken来源分析打开目标网站的开发者工具F12切换到 Network网络面板。在进行任何页面操作如登录、搜索、翻页前先清空现有记录然后触发一个数据加载动作比如点击查询按钮。很快你会看到一系列的网络请求。我们的目标是找到那个返回实际数据通常是JSON格式的XHRFetch请求。找到后重点查看它的Request Headers请求头。accesstoken极大概率就藏在Authorization或是一个自定义的X-Access-Token头字段里。记下这个请求的URL和请求方式GET/POST。接下来问题来了这个accesstoken是首次打开网页就有的还是通过某个登录接口获取的你需要顺着请求的调用栈Initiator往回找。在Network面板中选中那个数据请求查看其Initiator标签页它显示了是哪个脚本发起了这个请求。点击跳转到Sources源代码面板的对应位置。更常见的情况是accesstoken是在用户登录成功后由服务器返回并保存在前端的例如 localStorage 或 sessionStorage。因此你需要找到登录的请求。在Network面板中过滤XHR或Fetch请求尝试进行登录操作找到一个返回内容包含token、accessToken等字段的请求响应。这个响应体就是accesstoken的源头。实操心得很多网站的登录流程现在都异常复杂可能涉及图形验证码、滑块验证、或是一次性密码。如果登录逆向难度太大可以退而求其次观察accesstoken是否有一定的有效期并且是否在页面刷新后依然有效即存储在localStorage中。如果是你可以手动登录一次然后从Application面板的Local Storage里直接把accesstoken复制出来硬编码到你的爬虫脚本里临时使用。但这并非长久之计脚本需要定期手动更新token。2.2 解密函数与AES参数挖掘找到返回加密数据的接口后查看其Response响应。你看到的可能是一个看似乱码的字符串或者是一个JSON对象但其data字段的值是一长串非常规的字符密文。这时就需要在前端代码里找到解密的函数。在Network面板找到加载的JavaScript文件通常是app.xxxx.js或chunk-xxx.js这类经过打包的文件。由于代码可能被压缩和混淆直接搜索关键词是最高效的方法。在Sources面板按CtrlShiftF进行全局搜索。搜索哪些关键词呢AES相关AES、CryptoJS一个常用的前端加密库、decrypt、encrypt、mode、padding。响应处理相关查看数据请求的成功回调函数看其中是否有对response.data进行处理的代码可能会调用一个解密函数。特定字段名如果响应JSON中有像encryptedData这样的字段名直接搜索它。一旦找到疑似解密的函数比如decryptData(encryptedStr)就要仔细分析它。关键信息通常包括密钥Key用于AES加密解密的秘密字符串。它可能硬编码在代码里风险高但简单更可能是通过某个接口动态获取或是根据固定规则如时间戳生成。初始向量IV用于CBC等模式增加加密安全性。同样需要找到其值或生成方式。加密模式最常见的是CBC密码分组链接模式。填充方式最常见的是PKCS7在PKCS5 padding上扩展。前端CryptoJS库通常使用Pkcs7。输出格式密文通常以Base64或Hex十六进制字符串形式传输。一个典型的前端解密代码片段CryptoJS可能长这样// 假设这是经过格式化后的代码 function decryptData(ciphertextBase64) { var key CryptoJS.enc.Utf8.parse(这是一个16/24/32字节的密钥); var iv CryptoJS.enc.Utf8.parse(这是一个16字节的IV); var decrypted CryptoJS.AES.decrypt(ciphertextBase64, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return decrypted.toString(CryptoJS.enc.Utf8); }你的任务就是在混淆的代码中找到类似的逻辑并提取出key和iv。有时key和iv并非明文而是通过CryptoJS.enc.Utf8.parse或CryptoJS.enc.Hex.parse处理一个字符串得到的你需要找到那个原始字符串。避坑指南极度警惕“动态密钥”。有些网站会将当前时间戳、accesstoken的一部分或其他变量经过MD5或SHA256哈希后截取固定长度作为密钥。这意味着密钥每次请求都可能变化。你必须完整复现这个密钥的生成算法这是本项目最大的难点之一。如果发现密钥生成逻辑过于复杂且混淆严重需要评估项目成本。3. 核心工具选型与Python环境搭建逆向分析完成后我们就需要在Python环境中复现整个流程。工具链的选择至关重要。3.1 网络请求库Requests vs. PlaywrightRequests轻量级、速度快、资源消耗小是处理HTTP API请求的首选。它适用于接口参数规律、无需执行JavaScript的场景。在本项目中一旦我们获取到accesstoken并知晓数据接口URL用requests构造请求头携带accesstoken并发起请求是最直接的方式。Playwright / Selenium浏览器自动化工具。当登录流程极度复杂如需要处理图形验证码、复杂的JavaScript鉴权逻辑或者accesstoken的生成逻辑深深嵌在难以逆向的JS代码中时使用无头浏览器模拟真人操作是终极方案。你可以用它们来自动完成登录然后从浏览器上下文中提取出最终的accesstoken和必要的Cookie。我的选择与理由优先尝试纯requests方案。因为它的效率和稳定性远超浏览器自动化。只有当requests路径完全走不通如密钥生成算法无法逆向时才考虑引入playwright。本项目假设我们已经通过逆向分析找到了相对固定的密钥或可复现的密钥生成算法因此主要使用requests。3.2 加密解密库PyCryptodomePython中进行AES加解密的库主要有pycrypto已停止维护和它的继任者pycryptodome。pycryptodome功能完整API友好是行业标准选择。安装命令pip install pycryptodome requests如果遇到安装问题可以尝试使用国内镜像源pip install pycryptodome requests -i https://pypi.tuna.tsinghua.edu.cn/simple3.3 辅助工具正则表达式与JSON处理rePython内置的正则表达式模块用于从混淆的JavaScript代码中提取密钥、IV等字符串。jsonPython内置模块用于解析接口返回的JSON数据和解密后的明文数据。4. 实战代码从获取Token到解密数据假设我们已经通过分析得到了以下信息登录接口POST https://api.example.com/login 提交username和password返回{“access_token”: “your_token_here”}。数据接口GET https://api.example.com/data/market 需要在请求头中设置Authorization: Bearer your_token_here。返回的数据中encrypted_data字段是AES-CBC-PKCS7Padding加密后的Base64字符串。密钥Key通过分析JS发现是固定的字符串“MySuperSecretKey16”16字节。初始向量IV同样是固定的“InitVector16Byte”16字节。下面我们来编写完整的爬虫脚本。4.1 步骤一模拟登录获取accesstokenimport requests import json from urllib.parse import urljoin BASE_URL “https://api.example.com“ LOGIN_URL urljoin(BASE_URL, “/login”) DATA_URL urljoin(BASE_URL, “/data/market”) def get_access_token(username, password): “”“模拟登录获取accesstoken”“” login_payload { “username”: username, “password”: password } headers { “User-Agent”: “Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36”, “Content-Type”: “application/json” } try: response requests.post(LOGIN_URL, jsonlogin_payload, headersheaders, timeout10) response.raise_for_status() # 检查HTTP错误 result response.json() # 假设返回格式为 {“code”: 200, “data”: {“access_token”: “xxx”}} if result.get(“code”) 200: access_token result.get(“data”, {}).get(“access_token”) if access_token: print(“[成功] 获取到accesstoken”) return access_token else: raise ValueError(“响应中未找到access_token字段”) else: raise ValueError(f”登录失败返回码: {result.get(‘code’)}, 信息: {result.get(‘msg’)}”) except requests.exceptions.RequestException as e: print(f”[错误] 登录请求失败: {e}”) return None except json.JSONDecodeError as e: print(f”[错误] 登录响应不是有效的JSON: {e}”) return None # 使用示例 username “your_username” password “your_password” access_token get_access_token(username, password) if not access_token: print(“无法获取token程序退出”) exit(1)注意事项异常处理网络请求必须包含超时和异常处理避免脚本因单次请求失败而崩溃。JSON解析使用response.json()前最好用try-except包裹防止服务器返回非JSON内容如HTML错误页面导致解析失败。Token存储对于需要长期运行的脚本应将获取到的access_token及其过期时间如果有保存到文件或数据库中下次运行时先检查是否过期避免频繁登录触发风控。4.2 步骤二请求加密数据接口拿到access_token后我们用它来构造请求头获取加密数据。def fetch_encrypted_data(access_token, page1, size20): “”“携带token请求数据接口”“” headers { “User-Agent”: “Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36”, “Authorization”: f”Bearer {access_token}” # 注意格式可能是 ‘Bearer ‘ 或 ‘token ‘ } params { “pageNum”: page, “pageSize”: size # 根据实际接口添加其他参数 } try: response requests.get(DATA_URL, headersheaders, paramsparams, timeout15) response.raise_for_status() result response.json() # 假设返回格式为 {“code”:200, “encrypted_data”: “Base64StringHere”} if result.get(“code”) 200: encrypted_data_b64 result.get(“encrypted_data”) if encrypted_data_b64: print(f”[成功] 获取到第{page}页的加密数据”) return encrypted_data_b64 else: print(“[警告] 响应中未找到encrypted_data字段可能数据为空或结构已变”) return None else: print(f”[错误] 数据请求失败返回码: {result.get(‘code’)}, 信息: {result.get(‘msg’)}”) return None except requests.exceptions.RequestException as e: print(f”[错误] 数据请求失败: {e}”) return None except json.JSONDecodeError as e: print(f”[错误] 数据响应不是有效的JSON: {e}”) return None # 使用示例 encrypted_b64_string fetch_encrypted_data(access_token, page1) if not encrypted_b64_string: print(“无法获取加密数据程序退出”) exit(1)4.3 步骤三使用PyCryptodome进行AES解密这是最核心的一步。我们需要用Python复现之前在前端JS里看到的AES-CBC-PKCS7解密逻辑。import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import unpad def aes_cbc_decrypt(encrypted_b64, key_str, iv_str): “”“ 使用AES-CBC模式解密Base64编码的密文。 注意前端CryptoJS的PKCS7填充在Python中对应的是PKCS7或通用的unpad。 PyCryptodome的 unpad 函数处理的就是PKCS7。 “”“ # 1. 将字符串密钥和IV转换为字节并确保长度正确 # AES-128 需要16字节密钥 AES-192需要24字节 AES-256需要32字节。 # CBC模式需要16字节的IV。 key key_str.encode(‘utf-8’) iv iv_str.encode(‘utf-8’) # 这里可以添加长度检查或自动补齐逻辑根据实际情况 # 例如如果密钥不足16字节可以后面补零如果超过可以截断不推荐最好找到原版密钥。 # if len(key) 16: # key key.ljust(16, b‘\0’) # elif len(key) 16: # key key[:16] # 对iv做同样处理 # 2. 解码Base64密文 try: encrypted_bytes base64.b64decode(encrypted_b64) except Exception as e: print(f”[错误] Base64解码失败: {e}”) return None # 3. 创建AES解密器 cipher AES.new(key, AES.MODE_CBC, iv) # 4. 解密并去除填充 try: decrypted_padded_bytes cipher.decrypt(encrypted_bytes) # 使用unpad去除PKCS7填充 decrypted_bytes unpad(decrypted_padded_bytes, AES.block_size) except ValueError as e: # 可能填充不正确或者密钥/IV错误 print(f”[错误] 解密或去除填充失败: {e}”) # 可以尝试直接输出解密后的字节看看是否已经是明文但包含乱码 # print(“解密后字节未unpad:”, decrypted_padded_bytes) return None # 5. 将解密后的字节转换为字符串假设是UTF-8编码的JSON try: decrypted_text decrypted_bytes.decode(‘utf-8’) return decrypted_text except UnicodeDecodeError as e: print(f”[错误] 解密结果无法用UTF-8解码: {e}”) # 可能是编码问题尝试其他编码如 ‘gbk’ # return decrypted_bytes.decode(‘gbk’, errors‘ignore’) return None # 使用示例使用之前逆向得到的固定密钥和IV KEY “MySuperSecretKey16” # 确保这是16/24/32字节 IV “InitVector16Byte” # 确保这是16字节 decrypted_json_str aes_cbc_decrypt(encrypted_b64_string, KEY, IV) if decrypted_json_str: print(“[成功] 数据解密完成”) # 将解密后的JSON字符串解析为Python字典 data_dict json.loads(decrypted_json_str) print(json.dumps(data_dict, indent2, ensure_asciiFalse)) # 美化打印 else: print(“解密失败请检查密钥、IV或密文格式。”)4.4 步骤四整合与数据持久化将以上步骤串联起来并加入循环翻页、错误重试和数据保存的逻辑。import time import pandas as pd from typing import List, Dict def main(): # 1. 获取Token token get_access_token(“your_username”, “your_password”) if not token: return all_data [] max_pages 10 # 设置最大爬取页数防止无限循环 retry_max 3 for page in range(1, max_pages 1): print(f”\n正在处理第 {page} 页...”) encrypted_data None # 带重试的数据请求 for retry in range(retry_max): encrypted_data fetch_encrypted_data(token, pagepage) if encrypted_data: break else: print(f”第{retry1}次请求失败{‘重试…’ if retry retry_max-1 else ‘跳过该页。’}”) time.sleep(2) # 等待一段时间后重试 if not encrypted_data: print(f”第{page}页数据获取失败跳过。”) continue # 2. 解密数据 decrypted_str aes_cbc_decrypt(encrypted_data, KEY, IV) if not decrypted_str: print(f”第{page}页数据解密失败跳过。”) continue # 3. 解析JSON try: page_data json.loads(decrypted_str) # 假设解密后的结构是 {“list”: […], “total”: 100} data_list page_data.get(“list”, []) if not data_list: print(f”第{page}页无数据可能已爬取完毕。”) break all_data.extend(data_list) print(f”第{page}页成功解析 {len(data_list)} 条记录。”) # 礼貌性延迟避免请求过快 time.sleep(1) except json.JSONDecodeError as e: print(f”第{page}页解密后的内容不是有效JSON: {e}”) # 可以打印出解密后的字符串前500字符帮助调试 print(f”解密内容预览: {decrypted_str[:500]}”) continue # 4. 保存数据 if all_data: df pd.DataFrame(all_data) filename f”building_market_data_{time.strftime(‘%Y%m%d_%H%M%S’)}.csv” df.to_csv(filename, indexFalse, encoding‘utf_8_sig’) # 使用utf_8_sig避免Excel打开乱码 print(f”\n[完成] 共爬取 {len(all_data)} 条数据已保存至 {filename}”) else: print(“\n[完成] 未爬取到任何数据。”) if __name__ “__main__”: main()5. 高级问题排查与实战技巧在实际操作中几乎不可能一帆风顺。下面是我遇到的一些典型问题及解决方法。5.1 常见错误与解决方案问题现象可能原因排查步骤与解决方案登录失败返回验证码错误触发了反爬机制需要验证码。1. 检查请求头是否完整User-Agent, Referer, Content-Type等。2. 在浏览器中手动登录一次复制完整的请求头包括Cookie到爬虫中。3. 考虑使用playwright自动化浏览器处理登录获取登录后的Cookie和Token。获取到的accesstoken无效或过期快Token有很短的有效期或每次请求需要刷新。1. 分析登录响应看是否有expires_in过期时间字段。2. 实现Token刷新机制。如果有refresh_token接口定时刷新。3. 将Token获取逻辑封装成函数在每次请求数据前检查并更新。aes_cbc_decrypt解密失败报ValueError: Padding is incorrect.这是最高频的错误原因多样1. 密钥Key错误。2. 初始向量IV错误。3. 加密模式不是CBC。4. 填充方式不是PKCS7。5. 密文在传输或处理中被修改如多了解码步骤。1.核对Key/IV确保从JS中提取的字符串完全正确包括大小写和不可见字符。用repr(key_str)打印看看。2.核对模式与填充确认前端使用的是CBC和Pkcs7。3.检查密文确保传给解密函数的是纯Base64字符串没有多余的引号或空格。可以打印encrypted_b64_string[:100]看看。4.尝试无填充解密先注释掉unpad那行直接输出decrypted_padded_bytes看看最后几个字节是什么。PKCS7填充的字节值就是填充的长度。解密后得到乱码但长度似乎正确1. 编码问题。前端可能是UTF-16或GBK。2. 解密其实成功了但数据本身不是JSON可能是其他格式。1. 尝试不同的编码解码.decode(‘gbk’),.decode(‘utf-16’)。2. 将解密后的字节直接写入文件用文本编辑器或hexdump查看判断其结构。密钥是动态生成的最复杂的情况。密钥可能由Date.now()、token等变量经过哈希、截断生成。1. 在JS解密函数入口打调试断点观察调用时传入的密钥值。2. 搜索密钥生成函数。关键词key,iv,CryptoJS.enc.Utf8.parse的调用链。3. 如果JS混淆严重考虑使用execjs或PyExecJS库直接在Python中执行提取出的密钥生成JS代码片段。5.2 使用execjs执行JavaScript代码当密钥生成逻辑非常复杂用Python重写困难时可以将关键的JS函数提取出来用execjs在Python环境中执行。import execjs # 假设你从网站JS中提取出了生成密钥的函数字符串 key_gen_js_code “”” function generateKey(timestamp) { var str “someSalt” timestamp; // 一些复杂的哈希、转换操作... return CryptoJS.MD5(str).toString().substr(0, 16); } “”” # 创建一个JS上下文可能需要加载CryptoJS库 # 首先找到网站加载的CryptoJS库的源码通常是一个单独的 .js 文件将其内容保存为字符串 crypto_js_lib with open(‘crypto-js.min.js’, ‘r’, encoding‘utf-8’) as f: crypto_js_lib f.read() ctx execjs.compile(crypto_js_lib “\n” key_gen_js_code) # 调用JS函数 timestamp int(time.time() * 1000) # 模拟前端 Date.now() dynamic_key ctx.call(“generateKey”, timestamp) print(f”动态生成的密钥: {dynamic_key}”) # 然后使用这个dynamic_key进行解密重要提醒使用execjs性能较差且环境配置可能麻烦。它应该是解决动态密钥问题的最后手段。5.3 请求头与反爬策略现代网站的反爬不止于加密。你的爬虫还需要看起来像一个正常的浏览器。必备请求头User-Agent: 使用常见的浏览器UA字符串。Referer: 通常设置为目标网站的域名或上一级页面URL。Accept/Accept-Language/Accept-Encoding: 模仿浏览器。Content-Type: 对于POST请求根据接口要求设置application/json或application/x-www-form-urlencoded。Cookie管理requests的Session对象可以自动管理Cookie在登录后保持会话。session requests.Session() # 先使用session登录 session.post(LOGIN_URL, data...) # 后续请求会自动携带登录后的cookie response session.get(DATA_URL)请求频率控制在循环中增加time.sleep(random.uniform(1, 3))来模拟人类操作间隔避免被封IP。6. 项目总结与扩展思考走到这里一个能够自动获取Token、解密AES数据的爬虫就基本完成了。回顾整个过程技术核心在于逆向分析能力而不是单纯的编码。你需要耐心地使用开发者工具像侦探一样梳理网站的认证和数据流。这个项目模式具有很强的通用性。许多采用前后端分离、接口加密的网站或APP其数据抓取思路都是相通的分析认证流程获取凭证 - 定位数据接口 - 逆向数据解密/解压算法 - 用代码复现整个链条。对于更深入的需求你可以考虑以下扩展方向自动化与健壮性将脚本改造成定时任务加入更完善的日志、错误报警如邮件、钉钉机器人通知和断点续爬功能。数据清洗与入库解密后的JSON数据可能需要进一步清洗、去重、格式化然后存入MySQL、MongoDB或Elasticsearch等数据库便于后续分析。应对更复杂的加密如果遇到RSA非对称加密用于加密传输AES密钥、或WebSocket传输数据则需要学习相应的密码学知识和网络协议分析工具。道德与法律边界始终牢记爬虫应遵守网站的robots.txt协议尊重数据版权控制请求频率避免对目标服务器造成负担。将爬取的数据用于个人学习、研究或合法的商业分析切勿用于侵犯他人权益的用途。最后调试这类项目最需要的就是耐心和细致。一个字符的差异、一个字节的顺序都可能导致解密失败。多使用打印语句输出中间变量多和浏览器中正常运行的结果进行比对问题总能被定位和解决。希望这篇详尽的总结能帮你少走弯路。