尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python爬虫JS逆向实战:从抓包定位到算法复现与工程化

Python爬虫JS逆向实战:从抓包定位到算法复现与工程化 Python 爬虫里的 JS 逆向这几年被各种教程包装得神乎其神。抛开那些“秒变大神”的说法它真正解决的问题非常具体某个 Web 接口在发起请求前前端用 JavaScript 对参数做了一次或多次处理。处理结果可能是 sign、token、加密后的 data 字段也可能是一段带时间戳的签名。你的爬虫要拿到数据就得把这个处理过程复现出来。这类能力在接口调试、安全评估、开放平台 SDK 开发里都会用到前提是目标是你有权限访问的接口或者你正在调试自己维护的系统。这篇文章不针对任何具体商业平台拆解风控绕过方案只讲一套可以长期复用的方法论怎么定位加密参数、怎么识别标准算法、怎么用 Python 复现、批量请求时怎么处理工程化问题。适合已经会用 requests 发请求、但面对带签名的接口无从下手的开发者。1. 先定边界哪些逆向可以做哪些不能碰1.1 逆向在正规开发里到底指什么很多人一听到“JS 逆向”就联想到攻击某个大平台其实日常开发里最常见的逆向场景是这几类调试自己维护的前后端项目。前端代码做了加密或签名后端联调时你需要构造同样的加密请求来验证接口逻辑。企业安全测试。拿到授权后验证接口的签名机制是否可被篡改用来评估风险。基于开放 API 开发 SDK。很多开放平台的文档明确要求调用方按规则计算签名典型的就是 HMAC-SHA256 或 MD5。学习标准加密算法。在本地用固定样例复现 AES、RSA、MD5 的逻辑不针对任何线上目标。抓取完全公开、无需登录、无访问限制的数据。比如公开的开放数据接口、政府公开目录、官方提供的静态资源。判断一个操作能不能做不需要记太多法律条文先问自己三个问题目标服务允许程序访问吗请求频率会不会给服务器造成明显压力抓下来的数据用途合不合规这三个问题里有任何一个不明确就不要动手。1.2 建议主动避开的场景有些场景看起来“技术上能实现”但风险非常大不建议碰绕过登录态、付费墙或访问控制去拿本无权访问的数据。这类数据往往属于个人隐私、商业机密或版权保护内容。高频抓取影响目标服务器稳定性。即使目标没有反爬也不代表你可以无限请求。未经授权采集个人联系方式、交易记录、实名信息等敏感数据。破解影视、音乐、电子书、地图等版权内容的保护机制用于传播。这些红线不是劝退而是保护自己。爬虫技术本身没有原罪但使用场景决定边界。1.3 为什么“大厂逆向”不适合当学习入口标题里常出现“企业级大厂逆向”但我不建议新手从大厂目标入门。原因很现实大厂风控体系复杂涉及设备指纹、行为检测、验证码、WAF单纯还原签名远远不够。前端代码往往经过重度混淆函数名、变量名全是随机字符串很难定位逻辑。商业平台对异常请求的检测非常敏感你的一次试探性请求可能直接让对方给你换验证码甚至触发账号风险。更好的学习路径是从标准算法入手在本地搭一个带签名逻辑的测试服务用自己的代码去复现。等基本原理通了再以合规方式接触真实场景。2. 本地环境准备直接把调试工具链搭好2.1 Python 环境与常用库先确认本机 Python 版本在 3.9 以上。Windows 安装时记得勾选 Add Python to PATH否则后面命令行里找不到 python。需要安装的库按用途分开pip install requests pycryptodome rsa cryptographyrequests发 HTTP 请求。pycryptodome提供 AES、DES、MD5 等对称加解密和摘要能力。rsa本地生成 RSA 密钥对做加密解密验证。cryptography更底层的加密库处理证书和复杂密钥格式时常用。安装完成后在命令行执行python -c import requests; print(requests.__version__)能看到版本号说明环境没问题。2.2 浏览器开发者工具怎么用抓包分析主要依赖浏览器的 DevTools不需要额外装抓包工具。打开一个页面后按 F12 进入开发者工具重点看这几个面板Network 面板过滤 XHR 和 Fetch能看到页面发起的每个 AJAX 请求。Headers 区查看请求 URL、请求头、请求体和参数列表。Initiator 区点击 Request Call Stack 可以看是哪个 JS 函数发起了这个请求很多加密参数的定位从这里开始。Sources 面板可以搜索全部前端 JS 源码。Console 面板可以在当前页面上下文里手动执行 JS验证某个函数的输出。新手最容易犯的错是打开 Network 后看到一堆请求就懵了。记住顺序先找到目标接口再分析参数最后才去翻 JS。2.3 Node.js 环境的作用前端加密逻辑本身是 JavaScript直接用 Python 去翻译容易因为编码、进制、填充方式不一致而失败。遇到这种情况我一般会在本地装一个 Node.js先把原始 JS 逻辑跑通确认输出结果和页面一致再决定是翻译成 Python 还是保留 Node 调用。安装 Node.js 16 以上版本命令行执行node -v确认。后面会用到它来执行本地 JS 脚本。2.4 建议的项目目录结构爬虫逆向的练习项目建议按模块拆分目录别把代码全堆在一个文件里crawler-crypto/ ├── logs/ # 日志目录 ├── output/ # 结果输出目录 ├── scripts/ # 本地 JS 脚本 │ └── sign.js ├── demo_crypto.py # 算法复现示例 ├── http_client.py # 请求封装 └── sign_utils.py # 签名工具目录清晰的好处是排查问题快。日志归日志输出归输出脚本归脚本互不干扰。3. 从抓包开始拆解一个带签名的请求3.1 先找请求再找加密完整的分析链路应该是打开页面触发一次目标操作在 Network 面板里找到对应的 XHR 请求查看请求参数。如果请求参数里有几个字段是你没法直接写出的比如 sign、token、encrypt_data那这些字段大概率是 JS 动态计算的。接下来要做的不是猜而是去 Sources 面板里搜索关键词。比如你看到参数名叫sign就在 JS 源码里全局搜索sign或signature。如果看到参数值是一段很长的字符串可以搜索encrypt、md5、AES、CryptoJS这类关键词。这里要强调一个习惯不要一上来就翻整段 JS。先明确请求长什么样再按关键词缩小范围效率高得多。3.2 常见搜索关键词速查想找的东西搜索关键词时间戳Date.now, getTime, timestamp, time随机数random, nonce, uuid签名sign, signature, md5, hmac, sha加密数据encrypt, decrypt, CryptoJS, JSEncrypt编码处理base64, btoa, atob, encodeURIComponent密钥相关key, iv, secret, pubKey, privateKey搜索结果里如果出现多个匹配优先看被调用次数最多的函数。更准确的方法是回到 Network 面板在目标请求的 Initiator 里点进发起请求的 JS 函数位置直接看函数内的调用关系。3.3 用断点和控制台验证找到可疑函数后在 Sources 面板里给函数内部打断点重新触发请求观察几个关键变量参与计算的输入字段有哪些。常见的输入包括 URL 路径、请求体、时间戳、随机字符串。有没有额外拼接的固定字符串也就是俗称的“盐”。输出格式是什么32 位 hex、64 位 hex、Base64 字符串还是两者混合。在 Console 面板里可以手动执行这个函数传入不同参数看输出会不会变化。这样能快速判断它到底是不是纯函数逻辑还是依赖了 DOM 状态。3.4 小步验证不要一口气还原整个流程加密参数往往由多个步骤组合而来先拼接字符串再做 MD5再 Base64最后转大写。每一步都可能出错。我建议把每一步单独验证先用页面里的真实参数作为输入。在 Console 里手动执行完整 JS 函数记录输出 A。在 Python 里用同样的输入复现第一步记录输出 B。对比 A 和 B。不一致就检查字符编码、大小写、字段顺序、空值处理。只要固定输入下输出一致就说明逻辑还原正确。之后再引入时间戳、随机数这些动态参数。4. 常见标准算法识别与 Python 复现4.1 摘要类算法MD5、SHA-1、SHA-256摘要算法是最常见的签名方式。特点是任意长度输入固定长度输出无法逆向还原原文。MD5 输出是 32 位十六进制字符串。实际项目里常常不只是对单个字符串做 MD5而是先按规则拼接多个字段有时还要拼一个固定盐值。import hashlib def md5_hex(data: str) - str: return hashlib.md5(data.encode(utf-8)).hexdigest() print(md5_hex(hello))SHA-1 输出 40 位SHA-256 输出 64 位Python 里写法几乎一样print(hashlib.sha1(hello.encode(utf-8)).hexdigest()) print(hashlib.sha256(hello.encode(utf-8)).hexdigest())判断一个签名是不是摘要算法先看输出长度和字符集。32 位 hex 优先猜 MD540 位猜 SHA-164 位猜 SHA-256 或 HMAC-SHA256。4.2 Base64不是加密但到处都是Base64 是一种编码方式把二进制数据转换成可打印字符并不提供安全性。前端代码里常把加密后的密文再做一次 Base64 方便传输。import base64 raw hello.encode(utf-8) print(base64.b64encode(raw).decode())有两点要注意标准 Base64 和 URL-safe Base64 的区别。URL-safe 会把换成-把/换成_末尾的有时会被去掉。自定义编码表。CTF 题目里常出现替换 Base64 编码表的做法标准编码结果和前端输出对不上时就要检查前端代码里是否定义了自定义的 64 字节字符表。4.3 对称加密AES-CBC 与 AES-ECB对称加密是前端加密数据字段最常用的方式。AES 有几个关键参数任何一个不对都解不出来模式常见 CBC、ECB。CBC 需要 IVECB 不需要。密钥长度16 字节对应 AES-12824 字节对应 AES-19232 字节对应 AES-256。填充方式常见 PKCS7Python 里用pad和unpad处理。数据编码加密前输入是 UTF-8 还是 UTF-16。输出编码密文输出是 Base64 还是 hex。下面是一个完整的 AES-CBC 加密解密示例from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad import base64 key b0123456789abcdef # 16 字节 iv babcdef0123456789 # 16 字节 # 加密 cipher AES.new(key, AES.MODE_CBC, iv) plaintext hello.encode(utf-8) encrypted cipher.encrypt(pad(plaintext, AES.block_size)) print(base64.b64encode(encrypted).decode()) # 解密 enc_data base64.b64decode(加密后的字符串) cipher2 AES.new(key, AES.MODE_CBC, iv) decrypted unpad(cipher2.decrypt(enc_data), AES.block_size) print(decrypted.decode())如果前端代码里用了 CryptoJS它的 key 和 iv 通常直接写在附近但需要确认是 UTF-8 字符串还是 hex 字符串。CryptoJS 的 key 转换成 Python 时经常遇到“key 长度不对”的报错本质就是编码没转对。4.4 非对称加密RSARSA 属于非对称加密公钥加密、私钥解密。前端场景里目标通常把公钥硬编码在 JS 里用来加密一个短字符串比如临时密钥、手机号、密码。本地测试时可以自己生成密钥对import rsa pub, priv rsa.newkeys(512) enc rsa.encrypt(bhello, pub) print(enc.hex()) dec rsa.decrypt(enc, priv) print(dec.decode())实际项目里公钥来源可能是 JS 文件中的一段字符串也可能是接口动态下发。格式上常见 PKCS#1 或 SPKI处理时需要注意前缀。RSA 加密内容长度有限制所以一次只会加密很短的数据。如果看到前端用 RSA 加密一个很长超过 117 字节的内容通常不是纯 RSA而是先用对称加密再用 RSA 加密对称密钥。4.5 HMAC 签名HMAC 是带密钥的哈希算法开放平台 API 签名里很常见。逻辑通常是从请求参数中取出参与签名的字段。按字典序排列。拼成字符串拼接 AppSecret。用 HMAC-SHA256 计算摘要。import hmac import hashlib message a1b2 secret your_app_secret result hmac.new( secret.encode(utf-8), message.encode(utf-8), hashlib.sha256 ).hexdigest() print(result)HMAC 和普通 MD5 的区别在于它必须带密钥。如果只给了一个固定字符串参与计算那就是带盐的 MD5如果用的是秘钥对象参与计算那就是 HMAC。4.6 算法识别速查表输出特征可能算法32 位 hex全小写MD540 位 hexSHA-164 位 hexSHA-256 或 HMAC-SHA256含 、/、 的可打印字符串Base64 或 Base64 编码后的密文需要 key 和 iv按分块处理AES、DES用公钥加密固定短内容RSAbcrypt 样式字符串服务端密码哈希前端签名里很少出现网络上动不动提量子加密、AI 破解实际生产环境里前端请求签名最常见的就是摘要、对称、非对称这几类。把标准算法吃透比追逐各种玄学概念有用得多。5. 用 Python 写出可复现的签名函数5.1 先把输入固定下来复现签名时最忌讳一上来就用真实时间戳和随机数。因为每次运行结果都不一样出错了根本没法对比。正确做法是先固定输入在页面里抓一组真实请求参数把 timestamp 写死。把 nonce、uuid 等随机字符串也写死。在 Python 里用同样输入计算签名跟页面结果对比。只有固定输入下能完全一致说明算法逻辑还原成功。然后再把时间戳和随机数改成动态生成。5.2 一个典型的拼串加签名示例很多网站签名就是“字典 key 排序 拼接 固定盐 MD5”。这种逻辑很常见也适合作入门样例import hashlib def build_sign(params: dict, secret: str) - str: keys sorted(params.keys()) raw .join(f{k}{params[k]} for k in keys) key secret return hashlib.md5(raw.encode(utf-8)).hexdigest() params {name: python, page: 1} print(build_sign(params, test_secret))这个示例本身不代表任何真实网站但它展示了一个核心思路签名的本质是“输入规则 摘要算法”。只要搞清楚字段排序规则、拼接分隔符、盐值来源就能在 Python 里稳定复现。5.3 复杂 JS 逻辑可以用 Node 本地验证当一段 JS 逻辑非常复杂或者里面有大量闭包和混淆直接逐行翻译成 Python 性价比很低。这时可以把这段 JS 提取到本地 Node 脚本中先跑通再决定后续方案。一个简单的本地签名脚本// sign.js const crypto require(crypto); function makeSign(input) { const raw input -demo-secret; return crypto.createHash(md5).update(raw).digest(hex); } console.log(makeSign(process.argv[2]));Python 里用 subprocess 调用import subprocess result subprocess.run( [node, sign.js, hello], capture_outputTrue, textTrue, encodingutf-8, timeout10, ) print(result.stdout.strip())这里要明确一点把网上找来的脚本拿来本地跑只适合做验证和理解。真正落地到自己的项目里还是要整理成清晰的 Python 函数或独立服务而不是把一堆看不懂的混淆代码挂在那里。5.4 把签名和请求拆成独立模块工程上我会把签名逻辑、请求逻辑、日志逻辑拆开。这样做的好处是签名算法变了只改一个文件请求重试逻辑变了只改另一个文件互不影响。建议至少拆成四个模块sign_utils.py负责所有加解密和签名计算。http_client.py负责请求发送、重试、超时控制。logger.py负责记录每次请求的状态和耗时。main.py负责任务调度和结果落盘。6. 批量请求时的工程化问题6.1 频控和重试策略签名正确不代表可以随意请求。任何接口都有负载上限即使没有显式反爬高频请求也会给对方服务器带来压力还可能被限流。新手最容易犯的错是一上来就开并发。我建议先单线程跑每次请求间隔 0.5 到 2 秒观察一段时间。确认稳定后再考虑提高速度。重试要带指数退避而不是无限重试import time import random def request_with_retry(func, max_retry3): for i in range(max_retry): try: return func() except Exception as e: wait 2 ** i random.uniform(0, 1) time.sleep(wait) raise RuntimeError(f请求重试失败: {e})不要对成功结果重试只对网络异常、超时和明确的服务端错误重试。6.2 签名参数时效性大多数签名都会绑定时间戳比如 sign md5(path timestamp secret)。这类签名只在几分钟内有效所以必须在请求发出前最后一步生成不能提前批量生成好。如果发现签名一直失效优先检查三点本地系统时间是否准确。timestamp 的格式是秒还是毫秒。生成签名时用的字段顺序是否和目标完全一致。6.3 日志、输出命名和断点续跑批量任务真正要盯的不是单条请求成功与否而是整体任务能否稳定跑完。我一般会给每条请求分配唯一 ID日志里记录起止时间和结果。推荐日志格式2025-01-01 12:00:00 | 000123 | success | 0.32s 2025-01-01 12:00:03 | 000124 | retry_1 | timeout 2025-01-01 12:00:06 | 000124 | success | 0.41s输出文件名带上日期、批次和状态避免反复运行覆盖结果。支持跳过已抓取项比每次从头跑要省事得多。6.4 合法性检查不是一句空话批量请求开始前检查三件事目标站点的 robots.txt 是否明确禁止该路径。你的请求频率是否显著高于正常人操作。数据用途是否只用于个人学习、技术验证或已授权的业务。7. 常见报错与排查链路7.1 签名结果不对先分清楚是算法错还是输入错签名不一致时不要急着怀疑算法。先做一次固定输入对比如果固定输入下 Python 输出和页面输出一致说明算法没问题问题出在动态参数。如果固定输入下都不一致检查大小写、hex 和 Base64 的区分、URL 编码、空值处理。大小写和编码是最容易被忽略的两个坑。同一个字符串UTF-8 和 GBK 编码出来的 MD5 完全不同。7.2 返回空数据或风控提示出现验证码、空数据、风险提示时先停掉请求不要继续试探。排查顺序请求头是否完整。User-Agent、Content-Type、Referer 是不是真实浏览器值。请求频率是否过高。请求参数里是否有设备标识、浏览器指纹、加密的 Cookie 信息。如果目标要求登录态才能访问数据那这条数据本质上就不允许匿名抓取。这时应该优先找官方接口而不是想办法伪造登录态。7.3 本地 Node 执行 JS 报错常见原因有这几个node不在 PATH 里subprocess 找不到命令。JS 脚本依赖了浏览器对象比如 window、document、navigatorNode 环境里不存在。Windows 下 subprocess 输出编码不对读出来的中文是乱码。脚本用了第三方 npm 包但没有安装。解决办法是按顺序检查先确认node -v能执行再确认脚本里没有浏览器 API最后确认输出编码统一为 UTF-8。7.4 Python 加解密库报错pycryptodome 常见问题之前装过老版Crypto包出现导入冲突。建议pip uninstall crypto pycryptodome后重装。AES 报 key 长度错误说明 key 不是 16、24、32 字节。检查是不是把 hex 字符串直接当成字节用了。解密报填充错误说明 IV、key、模式或填充方式其中一个不对。7.5 一个通用的排查顺序遇到任何问题按这个链路走比乱试参数高效看现象是报错、超时、空数据还是验证码。看请求参数参数是否完整签名是否最新生成。看请求头UA、Referer、Content-Type 是否正常。看环境系统时间、字符编码、依赖版本、Node 版本。看频率是否触发限流。看算法固定输入下是否复现成功。8. 新手最容易踩的 5 个坑和后续学习建议8.1 五个坑第一个坑拿大厂目标练手。混淆程度高、风控强、失败率也高不适合入门。先在本地或开放接口上练习。第二个坑只看教程不动手。签名还原必须在本地跑通才算学会看十遍不如写一遍。第三个坑上来就开并发。很多简单问题会被并发放大成频率问题先单线程跑稳。第四个坑忽略输入编码。同一个字符串不同编码算出来的摘要完全不同这是新手最常踩的暗坑。第五个坑把伪造签名当成核心能力。真正值钱的是理解协议、算法和工程化不是记住某个网站的签名规则。8.2 建议的学习顺序如果从零开始按这个路径走比较稳熟练使用 requests 发送 GET、POST 请求。熟练使用浏览器 Network 面板分析接口。补前端 JS 基础函数、对象、作用域、闭包。掌握 MD5、SHA、AES、RSA、HMAC 五种标准算法每个都能写出 Python 示例。学一点 Node 基础能在本地跑 JS 脚本。搭建一个自己的练习项目比如给一个开放 API 写签名 SDK。再深入看加密参数定位、混淆代码分析、安卓抓包等进阶内容。8.3 最后说点实在的踩过几次坑之后我最大的体会是爬虫逆向技术真正值钱的部分不是你记住某个网站的加密流程而是你能不能快速定位问题、理解标准算法、把请求工程化。学习阶段宁可慢一点也要把每个样例在本地跑通。至于要不要对一个具体目标做逆向动手之前先问自己三个问题接口数据我有没有权限访问请求频率会不会影响对方服务数据用途合不合法。三个问题都确认没问题再开始。
返回列表