先聊几句题外话。很多刚开始接触爬虫的同学,常常会卡在同一个地方:请求头明明照着浏览器填了,Cookie 也带了,可服务器还是返回403或者data: [...]这种看不懂的密文。这个时候,大概率是遇到了JS 逆向问题——数据在浏览器端被 JavaScript 加密,服务器也只认加密后的结果。网上关于 JS 逆向的资料虽然多,但大多零散,要么只讲一个算法,要么直接扔一段扣好的代码,让人知其然不知其所以然。
所以我整理了这一篇以“标准算法 + 扣代码思路 + 完整实战”为主线的文章,把 JS 逆向里最常用的加密算法、定位方法、断点技巧、Python 还原实现都串起来。文章不会只堆代码,而是会解释每个案例的思考过程,方便你遇到新网站时能自己分析。内容偏基础,也适合刚入门想系统学习爬虫逆向的朋友。
1. JS 逆向到底在做什么
1.1 什么是 JS 逆向
JS 逆向,简单来说,就是分析网站前端 JavaScript 代码,弄清楚它对请求参数、请求头、响应数据做了哪些处理,然后把这些处理逻辑用 Python 或其他语言重新实现一遍,从而让脚本能够模拟浏览器行为,拿到服务器返回的真实数据。
举个例子。某个网站登录时,浏览器地址栏里看到的是:
POST https://example.com/api/loginPayload 是:
{ "username": "admin", "password": "a1b2c3..." }但你直接写 Python 代码提交明文密码,服务器会提示“密码错误”。打开浏览器开发者工具,在 Sources 里搜一下,就会发现页面里有一段 JS,做了类似这样的操作:
password = md5(password + salt);所以,真实请求里提交的 password 并不是明文,而是“明文 + 盐值”后的 MD5 值。JS 逆向要做的,就是找到这个salt和加密函数,然后在 Python 里复现。
1.2 JS 逆向的三大应用方向
概括起来,目前 Web 端的 JS 逆向主要分为三类:
| 方向 | 说明 | 常见场景 |
|---|---|---|
| 请求参数加密 | 对提交的参数做签名、加密 | 登录密码、搜索参数、分页参数 |
| 请求头加密 | 对请求头做签名或动态生成 | 反爬 Token、时间戳签名、Headers 校验 |
| 响应数据加密 | 服务器返回密文,前端解密后渲染 | 价格接口、订单接口、搜索列表 |
这三个方向并不互斥,很多网站会同时使用。例如先对参数做签名(MD5/SHA256),再对整个请求体做 AES 加密,响应数据再用 RSA 解密。这也是为什么我建议新手先把标准算法掌握熟练,再去研究魔改算法。
1.3 需要掌握的基础工具
正式进入案例之前,建议先把工具链准备好:
- Python 3.8+:推荐 3.9 或 3.10,新项目尽量不用 2.x。
- Node.js:用于运行、调试 JavaScript 代码,尤其是扣下来的加密函数。
- 浏览器开发者工具:Chrome 或 Edge 的 F12 面板。
- Charles / Fiddler:抓包工具,用于移动端或桌面端应用。
- Burp Suite:用于分析 HTTP 请求、重放请求,尤其在自测接口时很有用。
- Postman / Apifox:用于接口调试。
Python 环境中,常用到的库包括:
pip install requests pip install pycryptodome pip install execjs pip install nodejs说明:
execjs可以在 Python 中直接执行 JavaScript 代码,但性能一般。实际生产环境中更推荐“用 Python 重写算法”或“直接调用 Node.js 子进程”。
2. 爬虫入门:先会写一个标准 requests 请求
很多同学一上来就想搞复杂的 RSA、AES,但真正到项目里,你会发现 60% 的网站其实只做了最简单的参数校验,甚至连加密都没有。所以先掌握标准爬虫请求的写法非常重要。
2.1 环境准备
操作环境以 Windows / macOS 为例,Python 版本建议 3.9 以上。先确认环境:
python --version pip --version如果还没有安装 requests,执行:
pip install requests2.2 最小请求示例
我们以常用的requests库为例,写一个最基本的 GET 请求:
import requests url = "https://httpbin.org/get" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } resp = requests.get(url, headers=headers, timeout=10) print(resp.status_code) print(resp.text)这是一个非常简单的请求,但已经包含了两个核心点:
- 设置
User-Agent,避免被一些基础反爬拦截。 - 设置
timeout,防止请求卡死。
2.3 带参数的 POST 请求
实际爬虫中,更多请求是 POST 类型,需要传表单或 JSON 数据。
import requests url = "https://httpbin.org/post" data = { "username": "admin", "password": "123456" } resp = requests.post(url, data=data, timeout=10) print(resp.json())如果网站要求提交 JSON,使用json参数:
resp = requests.post(url, json=data, timeout=10)需要注意的是,data与json的编码方式不同,服务器解析方式也不同,具体要看抓包工具里原始请求的Content-Type。
2.4 Session 保持会话
涉及登录的网站,通常需要携带 Cookie。用requests.Session()可以自动管理 Cookie:
import requests session = requests.Session() login_url = "https://example.com/api/login" login_data = { "username": "admin", "password": "123456" } session.post(login_url, data=login_data) profile_url = "https://example.com/api/profile" resp = session.get(profile_url) print(resp.text)Session对象会自动保存服务端返回的 Cookie,并在后续请求中携带,这能减少很多手动处理 Cookie 的麻烦。
这一步做完后,你会发现大部分“无需加密”的网站已经能正常爬取了。接下来我们再进入逆向的核心场景。
3. 逆向前的准备:定位加密位置
3.1 通过抓包确认加密参数
假设你遇到了一个需要加密参数的接口。第一步永远是打开浏览器开发者工具——Network 面板,重新触发一次请求,找到对应的 XHR 请求。
例如,某网站搜索接口:
Request URL: https://example.com/api/search Request Method: POST Payload: { "keyword": "手机", "sign": "b2f0c9d1e8a74b6f9d3e..." }肉眼观察,keyword是明文,但sign是一串十六进制字符。这个sign很可能就是加密签名。接下来要做的,就是找到sign是怎么生成的。
3.2 全局搜索关键词
按Ctrl+Shift+F,在开发者工具的 Search 面板里搜索sign关键字,通常能找到相关函数。如果网站过了混淆,搜索不到,还可以尝试搜索:
- 加密特征值:
md5、sha1、sha256、AES、RSA、encrypt - 算法特征库:
CryptoJS、jsencrypt、bcrypt - 参数名特征:
sign、token、key、secret
以 CryptoJS 为例,很多网站会直接在 HTML 里引入:
<script src="https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/crypto-js.js"></script>这就意味着,你能直接在全局对象上找到CryptoJS。
3.3 通过 XHR 断点定位调用栈
如果全局搜索找不到,可以用 XHR 断点:
- 在 Sources 面板右侧,找到
XHR/fetch Breakpoints。 - 点击加号,输入目标接口的 URL 片段,比如
api/search。 - 刷新页面或重新触发请求,浏览器会在发起请求的那一行 JS 自动暂停。
- 在 Call Stack 面板中,层层向上查看调用栈,找到参数生成的位置。
这是目前最常用、也最不容易误判的定位方式。很多复杂的混淆网站,靠搜索关键词是搜不出结果的,但断点一定能触发。
3.4 分清“算法加密”和“环境校验”
在定位到加密函数后,不要急着扣代码。先判断一下,加密函数是纯粹的标准算法,还是依赖了浏览器环境。
- 标准算法:如 MD5、SHA256、AES、RSA,这些是纯数学运算,不依赖浏览器环境,可以直接用 Python 或 Node.js 重写。
- 环境校验:如
navigator.userAgent、window、document、canvas指纹,这些值在 Python 里并不存在,需要特殊处理,例如用execjs注入 mock 环境,或者扣取完整代码后手动补环境。
新手建议先从“标准算法”入手,这类案例占比很高,也最容易获得成就感。
4. 标准算法实战:Base64 与 MD5 签名
这一章开始进入实战。每个案例我会按照“接口特征 -> 定位方式 -> Python 实现 -> 运行结果”的顺序来写。
4.1 案例一:Base64 编码型参数
现在很多网站的搜索参数或翻页参数,并没有做真正的加密,只是用 Base64 把 JSON 编码了一下。虽然 Base64 严格来说不算加密算法,但它却是逆向入门最常见的“低垂果实”。
假设某网站搜索请求的参数如下:
https://example.com/api/list?data=eyJwYWdlIjoxLCJzaXplIjoyMH0=看到这种eyJ开头的数据,几乎可以立刻判断是 Base64。因为 Base64 编码后的 JSON 字符串,开头通常是eyJ(对应{")。
定位方式:在开发者工具中搜索btoa或Base64,就能找到类似代码:
function encodeParam(obj) { return btoa(JSON.stringify(obj)); }Python 实现:
import base64 import json def encode_param(page, size): payload = { "page": page, "size": size } # 先转为 JSON 字符串,再 Base64 编码 json_str = json.dumps(payload, separators=(",", ":")) encoded = base64.b64encode(json_str.encode("utf-8")).decode("utf-8") return encoded print(encode_param(1, 20))输出结果:
eyJwYWdlIjoxLCJzaXplIjoyMH0=这样,我们在 Python 端就还原了前端逻辑。
4.2 案例二:MD5 加盐签名
MD5 是目前最常见的签名算法之一。很多网站在提交表单时,会先把密码做一次 MD5,然后再加盐拼接,或者把请求参数按字典序排序后拼接成一个字符串,整体做 MD5。
假设某登录接口的参数是:
{ "username": "admin", "password": "e10adc3949ba59abbe56e057f20f883e", "timestamp": "1731234567890" }其中password的明文是123456,加密逻辑看起来是:
password = md5(password + timestamp);这里额外加了一个时间戳,所以每次密码的 MD5 值都不同,也算是动态签名的一种常见形式。
Python 实现:
import hashlib import time def md5_encrypt(text): md5 = hashlib.md5() md5.update(text.encode("utf-8")) return md5.hexdigest() password = "123456" timestamp = str(int(time.time() * 1000)) sign = md5_encrypt(password + timestamp) print("timestamp:", timestamp) print("sign:", sign)输出结果类似:
timestamp: 1731234567890 sign: 8a4b7f2c9e6d3f1a...需要特别注意的是,MD5 加盐时盐值放在前面、后面、还是中间,结果完全不同。实际逆向时,一定要去 JS 代码里确认拼接顺序。
4.3 案例三:SHA256 请求体签名
部分网站会对整个请求体做哈希签名。比如请求体是:
{ "keyword": "手机", "page": 1 }前端会把 JSON 字符串按特定规则排序后,做 SHA256,得到sign,再把sign放入 Header 或请求体。
伪装前的 JavaScript 可能如下:
const crypto = require('crypto'); function signRequest(params) { const keys = Object.keys(params).sort(); let raw = ''; for (let key of keys) { raw += `${key}=${params[key]}&`; } raw = raw.slice(0, -1); return crypto.createHash('sha256').update(raw).digest('hex'); }Python 实现:
import hashlib def sha256_sign(params): # 按 key 排序,拼接成 key=value&key=value 形式 sorted_keys = sorted(params.keys()) raw = "&".join(f"{key}={params[key]}" for key in sorted_keys) print("待签名串:", raw) sign = hashlib.sha256(raw.encode("utf-8")).hexdigest() return sign params = { "keyword": "手机", "page": 1 } print("签名:", sha256_sign(params))输出结果:
待签名串: keyword=手机&page=1 签名: 0e3a5c8f8b8c9e1d2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5eSHA256 是一种不可逆哈希算法,应用非常广。尤其是在一些大厂的开放平台 API 中,签名逻辑基本都是这种思路:参数排序 -> 拼接 -> 哈希 -> 放入请求头或参数中。
5. 对称加密实战:AES 与 DES
MD5 和 SHA256 属于哈希算法,主要用来做完整性校验和签名。而真正意义上的“加密”,是能在加密后还原明文的算法。Web 端最常用的对称加密就是 AES。
5.1 AES 的核心知识点
AES 对称加密需要掌握几个关键概念:
- 密钥长度:AES-128、AES-192、AES-256。
- 填充模式:PKCS7、PKCS5、ZeroPadding、NoPadding。
- 工作模式:ECB、CBC、CFB、OFB、GCM。
- 偏移量:CBC 模式下需要 IV,ECB 模式不需要。
网站里最常见的是 AES-128-CBC 和 AES-256-CBC,配合 PKCS7 填充。如果遇到 GCM 模式,还需要处理额外的 AuthTag,复杂度会高一些。
5.2 定位 AES 加密代码
AES 加密代码在浏览器里有几个明显特征:
- 引入了 crypto-js:
CryptoJS.AES.encrypt(...) - 引入了 WebCrypto API:
crypto.subtle.encrypt(...) - 自研了 S 盒替换逻辑(少见)
在开发者工具里搜索AES或CryptoJS,通常都能直接定位到加密函数。一个典型的加密逻辑:
const key = CryptoJS.enc.Utf8.parse("0123456789abcdef"); const iv = CryptoJS.enc.Utf8.parse("abcdef9876543210"); function encryptByAES(content) { const encrypted = CryptoJS.AES.encrypt(content, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); }这段代码的意思是:
- 密钥
key是一个 16 字节字符串。 - 偏移量
iv是另一个 16 字节字符串。 - 使用 CBC 模式,PKCS7 填充。
5.3 Python 还原 AES-CBC
Python 中推荐使用pycryptodome库。安装:
pip install pycryptodome下面是上述 JavaScript 逻辑的完整 Python 实现:
from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad import base64 def aes_cbc_encrypt(content, key, iv): # 将明文转为字节 data = content.encode("utf-8") key = key.encode("utf-8") iv = iv.encode("utf-8") cipher = AES.new(key, AES.MODE_CBC, iv) # PKCS7 填充 padded_data = pad(data, AES.block_size, style='pkcs7') encrypted = cipher.encrypt(padded_data) # 转 Base64 输出 return base64.b64encode(encrypted).decode("utf-8") def aes_cbc_decrypt(encrypted_b64, key, iv): encrypted = base64.b64decode(encrypted_b64) key = key.encode("utf-8") iv = iv.encode("utf-8") cipher = AES.new(key, AES.MODE_CBC, iv) decrypted = cipher.decrypt(encrypted) # 去掉填充 return unpad(decrypted, AES.block_size, style='pkcs7').decode("utf-8") key = "0123456789abcdef" iv = "abcdef9876543210" encrypted_text = aes_cbc_encrypt("hello js spider", key, iv) print("加密结果:", encrypted_text) decrypted_text = aes_cbc_decrypt(encrypted_text, key, iv) print("解密结果:", decrypted_text)运行结果:
加密结果: 3b7dceb0e1ba8ea4ca8491d33a1a0b76 解密结果: hello js spider这里有几个坑需要提醒:
- key 和 iv 的字节长度必须符合要求。AES-128 需要 16 字节,AES-256 需要 32 字节。
- CBC 模式的 IV 长度必须等于块大小,即 16 字节。
- 填充方式不一致会导致解密失败。JS 端如果使用的是
Pkcs7,Python 端也要对应选择pkcs7。
5.4 案例:登录密码 AES 加密
某网站在登录时,对密码做了 AES 加密。定位后,发现加密逻辑是:
let encryptedPwd = CryptoJS.AES.encrypt(plainPwd, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }).toString();此时我们不需要知道密钥是怎么来的,只需要在 JS 代码中找到固定的key和iv。比如:
var key = CryptoJS.enc.Utf8.parse("www.xxx.com.key"); var iv = CryptoJS.enc.Utf8.parse("www.xxx.com.iv!");那么 Python 就可以直接复用上面的aes_cbc_encrypt函数:
key = "www.xxx.com.key" iv = "www.xxx.com.iv!" pwd = "mypassword123" encrypted_pwd = aes_cbc_encrypt(pwd, key, iv) print(encrypted_pwd)这里只是示例,网上很多网站的 key 和 iv 长度实际上并不标准,比如某个字符串长度不是 16 字节,前端在 parse 后会自动截断或补零。遇到这种情况,Python 端也需要做同样的字节截断或补零处理。
6. 非对称加密实战:RSA
6.1 RSA 加密特征
RSA 是非对称加密,公钥加密、私钥解密。在浏览器端,服务器会把公钥下发给前端,前端用公钥加密敏感数据,后端再用私钥解密。
RSA 加密在 JS 中的典型识别特征:
- 出现
setPublicKey、encrypt等字眼。 - 引入
jsencrypt库。 - 出现
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...这样的公钥字符串。
一个典型的 JS 加密逻辑:
const encryptor = new JSEncrypt(); encryptor.setPublicKey(publicKey); const encrypted = encryptor.encrypt(password);6.2 Python 实现 RSA 加密
Python 中常用rsa或Crypto.PublicKey.RSA来实现。这里以pycryptodome为例:
from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 import base64 def rsa_encrypt(public_key_str, content): # 加载公钥 pub_key = RSA.import_key(public_key_str) cipher = PKCS1_v1_5.new(pub_key) # 加密 encrypted = cipher.encrypt(content.encode("utf-8")) return base64.b64encode(encrypted).decode("utf-8") public_key = """-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC... -----END PUBLIC KEY-----""" result = rsa_encrypt(public_key, "123456") print(result)有几点要注意:
- RSA 加密有长度限制。使用 1024 位密钥时,PKCS1_v1_5 最多只能加密 117 字节;2048 位密钥最多 245 字节。如果明文太长,需要分段加密。
- 如果 JS 端使用了
setPublicKey传入的是不带BEGIN PUBLIC KEY头的裸 Base64 字符串,Python 端需要先拼装成标准 PEM 格式再导入。 - JSEncrypt 默认使用 PKCS1 v1.5 填充。如果遇到 OAEP 填充,Python 端要改用
PKCS1_OAEP。
6.3 RSA 分段加密示例
长文本分段加密是实际项目中的常见需求。比如密码字段可能不长,但如果加密的是手机号+验证码+时间戳拼接的长字符串,就必须分段。
def rsa_long_encrypt(public_key_str, content, chunk_size=100): pub_key = RSA.import_key(public_key_str) cipher = PKCS1_v1_5.new(pub_key) content_bytes = content.encode("utf-8") encrypted_chunks = [] for i in range(0, len(content_bytes), chunk_size): chunk = content_bytes[i:i + chunk_size] encrypted_chunk = cipher.encrypt(chunk) encrypted_chunks.append(encrypted_chunk) return base64.b64encode(b"".join(encrypted_chunks)).decode("utf-8")分段大小需要根据密钥位数计算,1024 位密钥时,chunk_size最大 117;2048 位时最大 245。这个值写得太小不会出错,但会多一些冗余;写太大则会直接报错。
7. 进阶实战:从 JS 中扣取加密函数
前面的案例,我们都是根据算法逻辑在 Python 中重新实现。但有些网站的加密逻辑比较复杂,比如魔改的 Base64、自定的字符映射表、多层嵌套的混淆算法,这时手动重写往往比较费劲。更高效的做法是:直接把浏览器里的 JS 加密函数扣下来,用 Python 调用。
7.1 使用 Node.js 运行扣下的函数
假设我们从某网站扣下了这样一段加密函数:
const CryptoJS = require("crypto-js"); function encryptContent(content, secretKey) { const key = CryptoJS.enc.Utf8.parse(secretKey); const iv = CryptoJS.enc.Utf8.parse("1234567890abcdef"); const encrypted = CryptoJS.AES.encrypt(content, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); } // 导出供其他模块调用 module.exports = { encryptContent };保存为encrypt.js,在终端里执行:
npm init -y npm install crypto-js node -e "const { encryptContent } = require('./encrypt.js'); console.log(encryptContent('hello', 'abcdef0123456789'));"输出:
U2FsdGVkX1+4n9Z4cH8XpYxUO...这样我们就验证了扣下来的 JS 函数能独立运行。接下来在 Python 中调用它。
7.2 Python 调用 JS:execjs 方式
安装:
pip install PyExecJSPython 调用代码:
import execjs with open("encrypt.js", "r", encoding="utf-8") as f: js_code = f.read() ctx = execjs.compile(js_code) result = ctx.call("encryptContent", "hello", "abcdef0123456789") print(result)7.3 Python 调用 JS:Node.js 子进程方式
execjs虽然方便,但偶尔会遭遇编码问题或环境依赖问题。更稳的方式是直接调用 Node.js 子进程。
Python 端代码:
import subprocess import json def call_js_function(js_file, func_name, *args): # 将参数通过 stdin 传给 Node.js payload = json.dumps({ "func": func_name, "args": args }) result = subprocess.run( ["node", js_file], input=payload, text=True, capture_output=True, encoding="utf-8" ) if result.returncode != 0: raise RuntimeError(f"Node.js 执行失败: {result.stderr}") return result.stdout.strip() # 使用方式 encrypted = call_js_function("encrypt_cli.js", "encryptContent", "hello", "abcdef0123456789") print(encrypted)encrypt_cli.js内容:
const { encryptContent } = require('./encrypt.js'); process.stdin.on('data', (data) => { const input = JSON.parse(data.toString()); const func = global[input.func] || decryptContent || encryptContent; const result = func(...input.args); process.stdout.write(result); });这种方式的优势是:Python 与 Node.js 完全隔离,不会因为 JS 库的兼容性问题影响 Python 主流程。如果加密逻辑复杂,推荐优先用这种方式。
7.4 扣代码时的环境补齐
有些 JS 加密函数会依赖window、document、navigator等浏览器对象。扣出来后直接用 Node.js 运行,会报错window is not defined。
解决方法有两种:
- 在 Node.js 脚本顶部手动 mock 环境变量。
- 在 Python 中用
execjs配合外部 JS 运行时,先注入 mock 对象。
比如:
const window = { userAgent: "Mozilla/5.0 ...", navigator: {}, document: {} }; const navigator = { userAgent: "Mozilla/5.0 ...", platform: "Win32" }; // 然后引入加密函数这种方法属于“补环境”,是逆向里比较进阶的操作。平时练习时,可以刻意选择一些加密不依赖环境变量的网站,先练好基本扣函数能力,再慢慢接触复杂的补环境场景。
8. 常见问题与排查思路
逆向过程最让人头疼的不是算法复杂,而是报错后不知道从哪里入手。我把新手最常碰到的问题整理成一张表,方便你对照排查。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 请求返回 403 | 缺少请求头或 Cookie 校验失败 | 检查 User-Agent、Referer、Origin、Cookie |
| 返回内容是一串密文 | 响应数据被加密,前端解密后渲染 | 搜索decrypt、decode、JSON.parse定位解密函数 |
| Python 加密结果与浏览器不一致 | 算法顺序、填充模式、编码方式不同 | 对比多次加密结果,优先检查字符串拼接和 Base64 编码 |
控制台报xxx is not defined | 扣代码时缺少依赖函数或环境对象 | 补全依赖 JS 文件,或在 Node.js 中 mock 环境变量 |
| AES 解密报错 | key/iv 长度不对,或填充模式不一致 | 确认密钥、IV 字节长度,填充模式统一为 PKCS7 |
| RSA 加密报错 | 明文过长,超出单次加密上限 | 使用分段加密 |
| 浏览器能请求,Python 请求失败 | HTTP 版本、TLS 指纹或 headers 顺序不同 | 尝试使用curl_cffi或调整 headers 顺序 |
这里特别说一个新手容易犯的错误:很多人拿到加密函数后,直接在浏览器控制台复制执行结果,然后在 Python 里设定固定的值去请求。这样做虽然能跑通一次,但一旦加密参数携带时间戳或随机数,下一次请求就会失效。所以正确的做法,是在 Python 端动态计算加密参数,或者动态调用 JS 函数,而不是粘贴一次性结果。
9. 标准算法识别的快速判断口诀
实际逆向过程中,我们可以通过密文特征快速判断算法类型,减少试错时间。整理了几个常见特征:
| 特征 | 判断 |
|---|---|
以eyJ开头 | 大概率是 Base64 编码的 JSON,直接解码查看 |
| 32 位小写十六进制 | 大概率是 MD5 |
| 40 位小写十六进制 | 可能是 SHA1 |
| 64 位小写十六进制 | 大概率是 SHA256 |
有+、/、=的密文 | 大概率是 Base64 编码后的加密结果 |
| 密文长度远大于明文长度 | 可能是 RSA 或 AES 再做了 Base64 输出 |
密文开头有U2FsdGVkX1 | 这是 CryptoJS AES 加密的默认格式特征 |
这些特征只能作为初步判断,不能直接当成定论。比如 MD5 也可以是 32 位大写,AES 加密后也可以转成 Hex 而非 Base64。最终还是要以 JS 代码为准。
10. 逆向的原则与合规提醒
爬虫和 JS 逆向技术本身是中性工具,但在实际使用中需要注意边界。我从工程实践角度给几点建议:
- 只对你有权访问的网站进行逆向分析,比如自己公司的系统、已授权的接口,或公开的练习靶场。
- 不要将逆向技术用于绕过登录态、破解付费内容、抓取用户隐私数据,这些行为可能违反相关法律法规。
- 练习时优先选择公开的模拟站点,或使用本地搭建的靶场环境。
- 在生产环境中,控制请求频率,合理设置延时,避免给目标服务器造成压力。
- 如果目标网站有明确的 robots 协议或服务条款限制,应当遵守。
- 涉及账号密码、支付信息等敏感数据时,不要在代码中硬编码密钥和口令。
这里也顺便解释一下:很多大厂网站上看到的复杂加密,并不是为了“防爬”,而是为了保护用户数据和接口安全。我们学习 JS 逆向,更重要的是理解常见的加密算法和前端安全设计思路,而不是把它当作绕过一切限制的万能钥匙。
11. 总结与下一步学习路线
这篇文章从 JS 逆向的基础概念讲起,介绍了抓包定位、算法识别、Python 还原以及扣取 JS 函数的完整流程。核心内容包括:
- Base64、MD5、SHA256 等常见摘要与编码算法的识别和还原。
- AES 对称加密的密钥、IV、填充模式概念,以及 Python 端实现。
- RSA 非对称加密的公钥加载与分段加密。
- 从浏览器中扣取 JS 加密函数,并在 Python 中通过
execjs或 Node.js 子进程调用的方法。 - 常见报错的排查思路和算法特征判断口诀。
下一步,你可以按照下面的路线继续深入:
- 先熟悉 Node.js 的基本用法,能独立运行一个 JS 文件。
- 练习从简单网站中定位加密参数,用 Python 重写。
- 尝试扣取完整的 JS 文件,并补全依赖。
- 学习 AST 抽象语法树,用于处理混淆代码。
- 了解接口签名、时间戳防重放、行为验证码等进阶反爬手段。
最后想说的是,逆向这条路没有捷径,唯有多练。每遇到一个新网站,把它当作一道新的算法题,先观察,再定位,最后动手实现。你会发现,很多所谓的“魔改加密”,底层依然是标准算法。只要掌握了底层逻辑,剩下的都是耐心活。
如果你觉得这篇文章对你有帮助,可以收藏备用,也欢迎在评论区交流你在逆向过程中遇到的坑。下一篇我会继续整理更复杂的混淆对抗与代码补环境实战,感兴趣的话可以关注。