免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Python爬虫JS逆向进阶:MD5、AES与RSA加密实战解析

Python爬虫JS逆向进阶:MD5、AES与RSA加密实战解析 做爬虫方向的朋友普遍会经历这样一个过程刚开始用requests把静态页面抓下来觉得“爬虫不过如此”直到某天遇到一个带sign参数、几层加密字段或者动态 Token 的接口才突然发现自己连第一步都迈不过去。这时候再回头看之前掌握的 Python 语法、Requests 库、XPath 解析其实都只是爬虫的地基真正的分水岭是 JS 逆向。市面上流传的“全网最新超实用的 60 个 Python 爬虫 JS 逆向案例”这类合集听上去数量吓人实际拆开看核心套路是有限的。大量案例只是同一个加密思路换了不同网站外壳标准化算法、签名参数、请求体加密、响应数据解密、Cookie 生成、动态 Token翻来覆去就十几个套路。能把 30 个真正吃透剩下 30 个基本是重复劳动。这篇文章不打算罗列 60 个案例的流水账而是把这些案例背后的共性问题提炼成一套可迁移的方法论并用三个可本地运行的完整示例把 MD5 摘要、AES 对称加密、RSA 非对称加密从“听到过”变成“能复现”。读完你会明白面对一个加密接口该从哪里定位入口、怎么识别算法、怎么用 Python 还原请求以及什么时候该扣 JS、什么时候该用标准算法重写、什么时候干脆走 RPC。1. 为什么 JS 逆向是爬虫进阶的必修课先聊一个很实际的问题爬虫为什么要跟 JavaScript 较劲传统爬虫抓取逻辑是这样的分析 HTTP 请求构造参数发送请求解析响应。这套流程对纯服务端渲染的网站完全适用。但现在的前端框架越来越重很多数据已经不是 HTML 里躺着等你去抓而是通过 Ajax 接口动态返回。打开开发者工具看 Network 面板请求参数里赫然多了一堆你不知道怎么生成的字段{ account: 13800138000, password: 6f1f8f6e9a6fa8d4f5b1d6a3c2e8f1a0, timestamp: 1720000000000, nonce: a1b2c3d4, sign: 7d6f6dfc6e4a4f7aa1d6f0c4c0b2a9d8 }account和timestamp好办正常传就行。但password明显不是明文sign看起来像某种摘要nonce也不知道是固定的还是随机生成的。如果只用requests这个请求就永远发不出去。换言之爬虫爬不动往往不是请求问题而是模拟问题——你没法像浏览器那样“算出”目标网站期望的参数。JS 逆向解决的就是这个“模拟”问题。你要做的是找到浏览器里生成这些参数的 JS 代码读懂它的加密逻辑然后在自己的 Python 脚本里用等价方式重新生成一遍。于是爬虫进阶就变成了一项标准化工作定位入口、打断点、读代码、复现逻辑。这背后的价值非常清晰。能掌握 JS 逆向的开发者在数据采集、接口分析、自动化测试、风控研究等领域都更有主动权。因为无论技术怎么迭代前端总有代码要执行参数总要生成只要这个逻辑存在就有被分析和模拟的空间。2. 基本概念JS 逆向到底在逆什么在进入案例之前先把几个高频概念说清楚。很多新手在一开始就被术语劝退其实它们没那么抽象。JS 逆向指通过分析网页加载的 JavaScript 代码还原数据生成逻辑从而不依赖真实浏览器也能完成请求构造的技术过程。它的对象是前端逻辑而不是编译产物或后端服务。逆向加密指目标网站在前端对请求参数或响应数据做了加密处理爬虫需要在本地用同样的算法和密钥重新生成加密结果。广义上也包括解密从接口返回的密文。标准算法指 Base64、MD5、SHA、HMAC、AES、RSA、DES、SM2/SM3/SM4 这些公开的、有固定规范的加密算法。它们不是某家公司独有的任何开发者只要拿到密钥和参数都能在 Python、Java、Node.js 等任意语言中复用。这也是为什么“识别算法”比“读懂代码”更重要的原因。三个概念合在一起就是一个完整的链路目标网站用标准算法在前端处理了数据我们通过 JS 逆向定位到这段逻辑再用标准算法在 Python 中还原。你会发现真正困难的部分往往不在算法本身而在于如何从几十个 JS 文件中定位那段关键代码。这里必须给新手一个定心丸前端加密防不住真正愿意研究的人它只能拉高采集门槛。因为前端代码是“透明”的所有逻辑都要在用户浏览器里执行加密方式、密钥、生成规则都暴露在可观察的范围里无非是藏得深还是藏得浅的问题。60 个案例练习的价值就是让你快速熟悉各种“藏法”。3. 环境准备与基于工具链的定位思路工欲善其事必先利其器。JS 逆向需要一套组合工具缺少任何一个都会事倍功半。3.1 必备工具清单工具用途关键能力Chrome / Edge DevTools抓包、断点、调用栈分析Network、Sources、Overrides、SearchFiddler 或 mitmproxy移动端或网页端抓包HTTPS 解密、请求重放、脚本过滤Burp Suite接口安全测试与请求分析Proxy、Repeater、Comparer、编码转换Node.js本地运行和调试网页 JS与浏览器一致的 V8 引擎Postman / Apifox构造和验证请求参数预览、集合管理、环境变量Python 3.8脚本落地与算法复现requests、pycryptodome、execjs这里特别说一下Burp Suite它在热搜词中频繁出现也是逆向工作中容易低估的一环。很多同学只在测试接口时用一下 Repeater其实 Burp 的 Comparer 模块非常适合对比两次请求的加密参数差异Proxy 的历史记录可以快速筛选 JS 文件。遇到网页版抓包不方便的情况把流量代理到 Burp 会更顺手。3.2 Python 依赖安装本地的三个演示案例需要安装以下依赖。版本请以实际环境为准这里展示的是通用安装方式pip install requests flask pycryptodomerequests用于构造 HTTP 请求flask用于在本地启动一个模拟目标网站pycryptodome是 Python 中实现 AES、RSA 等标准算法的核心库。3.3 网页端加密逻辑的定位方法面对一个未知网站从哪里下手找加密函数把 60 个案例的经验总结起来无非以下几条路径全局搜索关键词。在 Sources 面板按CtrlShiftF搜sign、encrypt、password、token、md5、aes等关键词大多数加密函数命名都比较直白。XHR 断点。在 Sources 面板右侧 DOM Breakpoints 区域添加 XHR/fetch 断点当目标接口发起请求时代码会自动停在发送请求那一行沿着调用栈往上找就能看到加密函数。事件监听与 Hook。通过Object.defineProperty对window上的加密函数做 Hook在参数传入和返回值输出时打印调用记录能快速确认参数来源。比较请求。用 Burp 或 DevTools 分别发起两次只改一个字段的请求对比差异字段往往能定位到哪些参数是需要动态生成的。定位到加密函数之后再去看它调用了哪些标准算法整个逆向就完成了一半。4. 案例一MD5 摘要加密的模拟与复现先从一个最简单的场景开始模拟一个登录网站前端在提交前对username password salt做 MD5 处理生成sign后端校验。4.1 模拟目标网站下面是模拟服务端代码保存为server_md5.py# 文件路径server_md5.py from flask import Flask, request, render_template_string import hashlib app Flask(__name__) SALT demo_salt_2024 HTML !DOCTYPE html html headmeta charsetutf-8title模拟登录/title/head body h3模拟登录页/h3 script srchttps://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js/script script function login() { var username document.getElementById(username).value; var password document.getElementById(password).value; var sign CryptoJS.MD5(username password demo_salt_2024).toString(); var xhr new XMLHttpRequest(); xhr.open(POST, /login); xhr.setRequestHeader(Content-Type, application/x-www-form-urlencoded); xhr.send(username encodeURIComponent(username) password encodeURIComponent(password) sign sign); xhr.onload function() { alert(xhr.responseText); }; } /script input idusername placeholder用户名 / input idpassword typepassword placeholder密码 / button onclicklogin()登录/button /body /html app.route(/) def index(): return render_template_string(HTML) app.post(/login) def login(): username request.form.get(username, ) password request.form.get(password, ) sign request.form.get(sign, ) expect hashlib.md5((username password SALT).encode()).hexdigest() if sign expect: return {code: 0, msg: login success} return {code: 1, msg: sign error} if __name__ __main__: app.run(debugTrue, port5000)4.2 JS 端加密逻辑分析注意页面里的这句关键代码var sign CryptoJS.MD5(username password demo_salt_2024).toString();它拼接了用户名、密码、固定 salt然后计算 MD5 摘要。浏览器开发者工具里还能看到CryptoJS来自外部 CDN说明这是标准算法库不需要我们去扣整个 JS只要在 Python 里也用 MD5 即可。4.3 Python 爬虫复现spider_md5.py完整代码# 文件路径spider_md5.py import hashlib import requests username admin password 123456 salt demo_salt_2024 sign hashlib.md5((username password salt).encode()).hexdigest() resp requests.post( http://127.0.0.1:5000/login, data{ username: username, password: password, sign: sign, }, ) print(resp.json())4.4 运行与验证先启动模拟服务python server_md5.py再执行爬虫python spider_md5.py预期输出{code: 0, msg: login success}如果输出sign error优先检查 salt 是否一致以及 Python 拼接的字符串顺序是否和 JS 端完全一致。这个案例虽然简单但已经包含 JS 逆向的核心流程定位加密函数 → 确认标准算法 → 在 Python 中重写。5. 案例二AES 对称加密的参数还原MD5 只能保证完整性不能加密长文本。实际业务中如果整个请求体或者某个字段需要加密传输对称加密是常见选择尤其是 AES。5.1 模拟目标网站服务端代码保存为server_aes.py# 文件路径server_aes.py import base64 import json from Crypto.Cipher import AES from Crypto.Util.Padding import unpad from flask import Flask, request app Flask(__name__) KEY bthis_is_32bytes_demo_key_123456 IV b16bytes_demo_iv_ def aes_decrypt(ciphertext_b64: str) - str: cipher AES.new(KEY, AES.MODE_CBC, IV) raw cipher.decrypt(base64.b64decode(ciphertext_b64)) # 去掉 PKCS7 填充 return unpad(raw, AES.block_size).decode() app.post(/api/order) def order(): data request.get_json() encrypted data.get(data, ) try: plaintext aes_decrypt(encrypted) obj json.loads(plaintext) return {code: 0, data: obj} except Exception as exc: return {code: 1, msg: fdecrypt error: {exc}} if __name__ __main__: app.run(debugTrue, port5000)模拟页面的 HTML 不在服务端代码里体现前端逻辑可以这样理解目标页面在发起订单查询前会用 AES 加密一个 JSON 对象再把密文放入请求体data字段。页面里真实的 JS 加密逻辑类似// 浏览器端加密逻辑模拟 const key CryptoJS.enc.Utf8.parse(this_is_32bytes_demo_key_123456); const iv CryptoJS.enc.Utf8.parse(16bytes_demo_iv_); const plaintext JSON.stringify({ mobile: 13800138000, orderId: A10001, amount: 199.00 }); const encrypted CryptoJS.AES.encrypt(plaintext, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }).toString();这段代码透露出三个关键信息算法是 AES模式是 CBC填充方式是 Pkcs7。密钥是 32 字节偏移量 IV 是 16 字节都是固定字符串。传输方式是 Base64 字符串。5.2 Python 端 AES 加密重写Python 爬虫需要做同样的事情用相同的 key 和 iv以 CBC 模式加密明文 JSON并输出 Base64。# 文件路径spider_aes.py import base64 import json import requests from Crypto.Cipher import AES from Crypto.Util.Padding import pad KEY bthis_is_32bytes_demo_key_123456 IV b16bytes_demo_iv_ def aes_encrypt(plaintext: str) - str: cipher AES.new(KEY, AES.MODE_CBC, IV) # 密文长度必须是 16 的倍数所以需要 PKCS7 填充 encrypted cipher.encrypt(pad(plaintext.encode(), AES.block_size)) return base64.b64encode(encrypted).decode() payload { mobile: 13800138000, orderId: A10001, amount: 199.00, } encrypted aes_encrypt(json.dumps(payload)) resp requests.post( http://127.0.0.1:5000/api/order, json{data: encrypted}, ) print(resp.json())5.3 运行与验证依次启动服务和爬虫python server_aes.py python spider_aes.py预期输出{code: 0, data: {mobile: 13800138000, orderId: A10001, amount: 199.0}}这个案例里最容易踩坑的地方有两个key 长度和 iv 长度不匹配会导致报错比如 key 必须恰好是 16/24/32 字节Python 的pycryptodome不会自动补位必须显式调用pad而 JS 的 CryptoJS 默认会补 Pkcs7两者要对应上否则服务端解密会失败。6. 案例三RSA 非对称加密的密钥还原如果说 AES 这类对称加密考验的是“密钥和模式要匹配”那 RSA 这类非对称加密考验的则是“公钥从哪来、填充方式是什么”。RSA 在登录和支付场景中非常常见因为它安全性更高前端只用公钥加密私钥永远留在服务端。6.1 生成测试密钥对本地演示先用 OpenSSL 生成一对 1024 位测试密钥生产环境中网站会自己生成并下发公钥我们只需要拿到公钥字符串openssl genrsa -out rsa_private.pem 1024 openssl rsa -in rsa_private.pem -pubout -out rsa_public.pem6.2 模拟服务端与前端逻辑服务端持有私钥对收到的加密字段解密# 文件路径server_rsa.py import base64 import json from Crypto.Cipher import PKCS1_v1_5 from Crypto.PublicKey import RSA from flask import Flask, request app Flask(__name__) with open(rsa_private.pem, rb) as f: private_key RSA.import_key(f.read()) cipher_pkcs1 PKCS1_v1_5.new(private_key) app.post(/api/order) def order(): encrypted request.get_json().get(encrypted, ) try: plaintext cipher_pkcs1.decrypt(base64.b64decode(encrypted), None) return {code: 0, data: json.loads(plaintext)} except Exception as exc: return {code: 1, msg: fdecrypt error: {exc}} if __name__ __main__: app.run(debugTrue, port5000)浏览器端 JS 通常用 JSEncrypt 库加密类似// 浏览器端加密逻辑模拟 const publicKey -----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC...\n-----END PUBLIC KEY-----; const encrypt new JSEncrypt(); encrypt.setPublicKey(publicKey); const encrypted encrypt.encrypt(JSON.stringify({ orderId: A10001, amount: 199.00 }));注意RSA 每次加密结果都不一样因为 PKCS1 填充引入随机数。所以不要通过对比密文是否一致来验证算法只要服务端能成功解密即可。6.3 Python 端 RSA 加密重写# 文件路径spider_rsa.py import base64 import json import requests from Crypto.Cipher import PKCS1_v1_5 from Crypto.PublicKey import RSA with open(rsa_public.pem, rb) as f: public_key RSA.import_key(f.read()) cipher_pkcs1 PKCS1_v1_5.new(public_key) payload json.dumps({ orderId: A10001, amount: 199.00, }).encode() encrypted base64.b64encode(cipher_pkcs1.encrypt(payload)).decode() resp requests.post( http://127.0.0.1:5000/api/order, json{encrypted: encrypted}, ) print(resp.json())6.4 运行与验证python server_rsa.py python spider_rsa.py预期输出{code: 0, data: {orderId: A10001, amount: 199.0}}这个案例的核心结论是RSA 逆向的重点不是还原算法而是找到公钥和识别填充标准。公钥通常以 PEM 字符串形式躺在 JS 变量里或者由接口下发填充方式常见的是PKCS1_v1_5和OAEP在 Python 里对应不同类选错了解密会失败。7. 扣代码、补环境与 RPC三种主流落地方式对比通过上面三个案例你应该已经发现只要确认了标准算法用 Python 重写往往比直接扣 JS 更干净。但现实中的网站不会总是这么“标准”有时你会遇到自定义混淆算法、多层变形、依赖浏览器对象的情况。这时候就需要选择不同的落地方式。7.1 方式一完整扣代码把目标网页的关键 JS 函数复制到本地用execjs或Node.js直接调用。适合目标网站使用了比较独立的加密函数不依赖太多浏览器 API 的场景。import execjs ctx execjs.compile(open(encrypt.js, encodingutf-8).read()) sign ctx.call(getSign, admin, 123456) print(sign)优点是还原度高缺点是 JS 代码可能依赖window、document、navigator等浏览器对象本地没有这些环境会报错。7.2 方式二补环境当扣下来的 JS 报window is not defined或navigator is not defined时可以在本地 JS 文件中提前声明这些对象或者用jsdom模拟浏览器环境。补环境的技术含量比单纯扣代码高因为它需要你理解代码到底用了哪些浏览器 API并给出合理假值。核心经验是补了报错报错少了再补直到不报错且结果与浏览器一致。7.3 方式三RPC 远程调用如果加密逻辑实在复杂或者 JS 严重依赖浏览器环境可以在浏览器里注入一段脚本把加密函数暴露出来由本地 Python 通过 WebSocket 等方式远程调用浏览器执行加密。这种方式绕过了补环境的麻烦但需要维护一个浏览器常驻进程且对目标网站来说没有本质区别——它该做多少计算还是做多少。三者的对比落地方式难度速度适用场景Python 重写标准算法低最快能识别出明确的标准算法时扣 JS execjs中中等算法复杂但依赖环境少时补环境高较慢依赖大量浏览器 API 时RPC 远程调用中高最慢加密非常复杂、环境难以模拟时在实际案例中很多团队的路线是先尝试标准算法重写再尝试扣 JS最后才考虑补环境和 RPC。因为标准算法重写的代码可维护性最高速度也最快这才是“大厂逆向加密案例”中真正值得反复练习的能力。8. 大厂逆向加密场景中更复杂的几个难点60 个案例里难度最高的往往不是某个网站的参数加密而是大厂场景中常见的组合拳。这里梳理几个重点但不会展开攻击方法只讲技术原理帮助建立认知。WebAssembly 加密部分网站把核心加密逻辑编译成.wasm文件前端 JS 只负责调用。特征是在网络面板里能看到.wasm请求。逆向时需要用浏览器 DevTools 的 wasm 调试能力或者直接尝试在 Node.js 中加载 wasm 模块。AST 混淆JavaScript 代码经过抽象语法树混淆后变量名、函数名、字符串常量都会变成无意义的短字符直接阅读源码很难。特征是代码里大量_0x3f2a形式的变量名。工具层面可以使用webcrack等开源项目做还原。动态 Token 与指纹采集登录后返回的 Token 可能与用户行为、时间戳、设备信息绑定重新采集时需要一起生成不能只靠静态分析。字体反爬与 Canvas 指纹这两类技术不加密请求但会让响应数据“看起来对不上”。字体反爬通过自定义字体文件将数字映射到新的字形Canvas 指纹则用于识别无头浏览器。遇到这些情况首先要有耐心其次要回到基础不管表面多复杂底层算法、时间戳、随机数和拼接顺序这些要素是绕不开的。60 个案例练到最后练的就是从混乱代码中识别这些要素的敏感度。9. 常见问题与排查思路把大量案例中反复出现的报错和诡异现象整理成一张表遇到问题按表排查效率最高问题现象可能原因排查方式解决方案扣下来的 JS 运行报错依赖浏览器 API 未定义查看报错行检查window/document/navigator手动补环境或改用 RPCPython 的 AES 加密结果与浏览器不一致模式、填充或编码不一致对比 key、iv、模式、填充方式统一为 CBC Pkcs7 UTF-8RSA 加密后服务端解密失败填充模式不同或密钥不匹配确认是 PKCS1_v1_5 还是 OAEP更换对应密文类接口返回数据是一串密文响应数据也做了加密搜索响应处理 JS 中的decrypt函数在 Python 里实现解密逻辑同一个请求多试几次sign 每次都不同加密逻辑引入了随机数或时间戳对比两次请求的参数差异提取动态参数生成规则使用execjs调用 JS 结果时慢Python 与 Node.js 进程通信开销统计调用耗时改用标准算法重写或增加缓存请求被风控限制请求频率或指纹特征明显检查返回状态码和响应提示降低频率、设置合理代理优先保证合法合规找不到加密函数入口搜索关键词命名太隐蔽使用 XHR 断点全局搜索在发起请求的调用栈中向上定位其中加密结果不一致是最容易让人心态崩溃的问题。我的建议是先把一段固定明文用浏览器执行一遍获得基准密文再在 Python 里执行自己的实现并输出中间步骤里的 Base64、key、iv、padding 值逐段比。绝大多数不一致最后都能归结为某个编码或填充的细节差异。10. 最佳实践与工程化建议如果想系统提升爬虫与 JS 逆向能力建议从第一天起就按照工程化的标准要求自己而不是停留在“跑通一次”的层面。第一建立自己的加密算法速查表。每遇到一个新案例先判断它属于 Base64/MD5/SHA/AES/RSA/SM 系列中的哪一类再记录识别特征。比如 RSA 密文长度通常与密钥长度相关AES-CBC 密文是 16 字节的倍数MD5 密文永远是 32 位十六进制。有了这套速查表后续看代码会快很多。第二优先用 Python 重写而不是无脑扣 JS。标准算法重写后代码只有几十行不需要依赖 Node.js 环境也没有浏览器对象缺失的问题。只有当你发现目标加密逻辑高度自定义或者算法是私有实现时才考虑扣 JS。第三把加密逻辑封装成独立模块。比如把sign生成、AES 加密、RSA 加密分别写成函数统一入参和返回格式。60 个案例练完后你会发现自己手里已经有了一套可复用的加密模块遇到新网站往往只需要改密钥和拼接规则。# 文件路径crypto_utils.py import base64 import hashlib from Crypto.Cipher import AES, PKCS1_v1_5 from Crypto.PublicKey import RSA from Crypto.Util.Padding import pad def md5_sign(*parts: str, salt: str ) - str: raw salt.join(parts) if salt else .join(parts) return hashlib.md5(raw.encode()).hexdigest() def aes_encrypt(plaintext: str, key: bytes, iv: bytes) - str: cipher AES.new(key, AES.MODE_CBC, iv) return base64.b64encode(cipher.encrypt(pad(plaintext.encode(), AES.block_size))).decode() def rsa_encrypt(plaintext: bytes, public_key_pem: bytes) - str: key RSA.import_key(public_key_pem) cipher PKCS1_v1_5.new(key) return base64.b64encode(cipher.encrypt(plaintext)).decode()第四注意频率控制和日志脱敏。抓取类程序要设置合理的请求间隔避免给目标站点带来压力日志中不要打印完整密码、Token、Cookie 等敏感信息。这既是工程习惯也是职业底线。第五合规必须放在第一位。爬虫技术本身是中性的但使用场景决定边界。以下几条是我认为所有做爬虫方向的开发者都应该遵守的只采集公开、合法、已授权或符合目标站点服务条款的数据。不绕过登录认证、不破坏网站正常功能、不利用漏洞获取非公开数据。使用爬虫时尊重robots.txt等平台的访问规则。对涉及个人信息的字段做脱敏处理不用于不当用途。技术研究尽量在本地模拟环境或自己的项目中进行。JS 逆向能力的价值在于让你理解 Web 应用的前端数据流和安全设计而不是教你采用非法手段获取数据。真正厉害的人是能在合法边界内把技术用到极致。最后针对“60 个案例从哪里开始练”这个问题给出一个比较实用的学习路径先把本文中的 MD5、AES、RSA 三个案例本地跑通然后按照“摘要算法 → 对称加密 → 非对称加密 → 混合加密 → 自定义混淆 → 反爬对抗”的顺序在每个类别中选几个高代表性案例精练而不是盲目追求案例总量。每做完一个案例就把它归档到自己的方法论体系里。等你能在五分钟内定位到加密入口、很快判断出算法类型的时候那 60 个案例的清单值不值你已经不需要别人回答了。
返回列表