1. 项目概述:为什么我们要研究JS加密
最近在做一个金融数据聚合的小工具,需要从几个主流平台获取一些净值数据来做分析。私募排排网作为国内知名的私募基金信息平台,自然是我重点关注的对象。但和很多现代网站一样,它的数据接口都做了加密处理,直接用
requests
库去请求,拿回来的要么是乱码,要么就是一堆看不懂的加密字符串。这其实就是典型的JavaScript(JS)加密,前端通过JS对请求参数或响应数据进行混淆和加密,以此来增加数据抓取的难度,保护接口安全。
对于做数据分析、量化研究或者只是想自动化获取公开数据的开发者来说,这成了一个不大不小的门槛。你明明看到浏览器里数据展示得好好的,但自己写脚本去抓就是拿不到。所以,今天我就来分享一下,我是如何一步步逆向分析私募排排网某个数据接口的JS加密逻辑,并最终用Python成功复现,实现稳定数据抓取的整个过程。我会把核心的逆向思路、关键代码的定位方法,以及完整的Python实现代码都贴出来,即使你是刚接触逆向的新手,跟着做一遍也能掌握这套方法论。
2. 逆向分析的核心思路与准备工作
逆向JS加密,听起来很高深,其实核心思路就一句话:
找到浏览器中执行加密的那个关键函数,理解它的输入输出和内部逻辑,然后用另一种语言(这里是Python)重新实现它。
2.1 核心思路拆解
这个过程可以分解为几个明确的步骤:
定位网络请求
:在浏览器开发者工具中,找到我们想要获取数据的那条网络请求(通常是XHR或Fetch请求)。查看它的请求头、请求参数和响应体,确认哪些部分是被加密的。
搜索加密入口
:在网站的JS源代码中,搜索与加密参数相关的关键词(如参数名、加密后的字符串片段等),或者通过“调用栈”来定位生成这些参数的JS函数。
分析加密函数
:找到关键函数后,需要静态分析(阅读代码)和动态调试(打断点、单步执行)相结合,弄清楚这个函数接收什么参数,内部经过了哪些处理(比如用了什么加密算法,是否调用了外部库,是否有随机因子),最后输出了什么。
模拟与复现
:在完全理解算法后,用Python编写代码,模拟整个加密过程。这里的关键是确保Python环境拥有JS代码所依赖的所有算法库(如CryptoJS、各种哈希、AES、RSA等),并且能正确处理字符串编码、字节转换等细节。
2.2 工具准备
工欲善其事,必先利其器。你需要准备好以下工具:
-
浏览器
:推荐使用
Google Chrome
或
Microsoft Edge
(Chromium内核)。它们的开发者工具功能最强大,对JS调试的支持也最好。
-
开发者工具
:重点是
“Network”(网络)
面板和
“Sources”(源代码)
面板。网络面板用来抓包,源代码面板用来调试JS。
-
代码编辑器
:用于编写Python代码,比如VS Code、PyCharm都可以。
-
Python环境
:确保安装了Python 3.6以上版本。我们主要会用到
requests
库进行网络请求,以及
execjs
库来执行JS代码片段(有时直接调用JS引擎比完全用Python重写更简单)。当然,也可能用到
hashlib
,
hmac
,
base64
等标准库。
# 安装必要的Python库
pip install requests execjs -
一个能执行JS的环境(备选)
:
execjs
需要一个JS运行时。在Windows上,它通常会尝试使用系统自带的JScript(IE引擎),但功能有限。我强烈推荐安装
Node.js
,
execjs
会自动检测并使用它,功能更完整。
注意:如果你在分析过程中发现加密逻辑过于复杂,直接使用
execjs
调用原网站的JS代码可能是最稳妥快捷的方式。但我们的最终目标是尽可能用纯Python实现,这样部署和运行效率更高。
3. 实战:逆向私募排排网数据接口
下面,我们进入实战环节。我以抓取“私募排排网”上某个基金列表页的数据为例。请注意,网站的具体加密方式可能随时间更新,但方法论是通用的。
3.1 第一步:抓包与观察
F12
或
Ctrl+Shift+I
打开开发者工具,切换到
Network(网络)
面板。记得勾选上
“Preserve log”(保留日志)
,防止页面跳转时请求记录被清空。
Type(类型)
是
xhr
或
fetch
,
Initiator(发起者)
列会显示
script
或其他JS文件。你可以通过预览响应内容来快速定位。找到那个响应体是JSON格式,并且包含基金名称、代码、净值等数据的请求。
Headers(标头)
和
Payload(负载)
。重点看:
-
Request Headers
: 有没有特殊的自定义Header,比如
X-Sign
,
X-Timestamp
,
Authorization
等。
-
Query String Parameters / Form Data
: 查看URL参数或请求体参数。你可能会发现一些看起来像加密字符串的参数,例如
sign
,
token
,
data
等,它们的值通常是一长串无规律的字符。
假设我们找到了一个关键的请求,它的URL里包含一个
sign
参数,或者请求体里有一个加密的
data
字段。我们的目标就是找出这个
sign
或
data
是如何生成的。
3.2 第二步:定位加密关键函数
这是逆向中最需要耐心和技巧的一步。
方法A:关键词搜索
Sources(源代码)
面板。
Ctrl+Shift+F
打开全局搜索框(注意,如果页面JS文件过多且被混淆,这个搜索可能会很慢或搜不到)。
sign
,进行搜索。在结果中,寻找那些看起来在
赋值
或
生成
这个参数的代码行。例如
sign: xxxx
,
params['sign'] =
,
var sign =
。
方法B:调用栈回溯(更有效)
Network
面板,找到那个数据请求,右键点击它,选择
“Copy” -> “Copy as cURL (bash)”
。这能帮你快速在Python中构造一个相同的请求,但缺少动态参数。
Initiator
列,通常有一个链接,点击它可以跳转到发起这个请求的JS代码位置。
Sources
面板,找到所有JS文件(通常被合并或混淆成一个
app.xxxx.js
或
vendor.xxxx.js
)。在文件开头部分,输入
debugger;
语句(如果网站没有反调试)或者使用XHR断点。
Sources
面板,找到右侧的
“XHR/fetch Breakpoints”
。点击
+
号,输入包含目标接口URL关键词的字符串(如
/api/fund/list
)。这样,当任何JS代码发起包含该关键词的请求时,浏览器会自动暂停(断住)。
Call Stack(调用栈)
。调用栈显示了当前断点位置是由哪一系列函数调用导致的。从栈顶往下看,你就能一步步回溯到最初生成加密参数的那个函数。
通过调用栈,我最终定位到了一个名为
generateSign
或
encryptData
的函数(函数名可能被混淆,比如变成
a0x12bc
之类的)。点击它,我们就进入了核心的加密逻辑代码区。
3.3 第三步:分析与理解加密逻辑
现在,我们面对的可能是一段被混淆压缩的JS代码。它可能长这样:
function s(t, e) {
var n = (new Date).getTime();
var r = Object.keys(t).sort().map(function(key) {
return key + "=" + t[key]
}).join("&");
var i = r + "×tamp=" + n + "&secret=" + e;
return a(i)
}
function a(t) {
// … 这里可能是MD5、SHA256或HMAC等哈希计算
}
我们需要分析这段代码:
函数输入
:
t
是参数对象,
e
可能是一个固定的密钥(secret)。
逻辑步骤
:
a. 获取当前时间戳
n
。
b. 将参数对象
t
的键按字母排序,然后拼接成
key1=value1&key2=value2…
格式的字符串
r
。
c. 将排序后的参数字符串、时间戳和密钥用
&
连接,得到待加密字符串
i
。
d. 将字符串
i
传入函数
a
进行处理。
函数
a
:我们需要进入
a
函数内部,看它具体做了什么。可能是
CryptoJS.MD5(i).toString()
,也可能是
CryptoJS.HmacSHA256(i, e).toString()
。这里就需要动态调试了。
动态调试
:在
generateSign
函数内部和
a
函数入口打上断点(点击行号即可)。重新触发请求,让代码再次断住。然后使用
Step Over (F10)
,
Step Into (F11)
单步执行,同时观察右侧
Scope
面板中变量的值变化。特别是查看
i
的值和
a(i)
的返回值。这能让你确切地知道输入是什么,输出是什么。
经过分析,假设我们确定加密逻辑是:
sign = MD5(排序后的参数字符串 + “×tamp=” + 时间戳 + “&secret=固定的密钥”)
。
实操心得:很多网站的签名算法并不复杂,核心就是“排序+拼接+哈希”。密钥(secret)有时是硬编码在JS里的,有时需要从其他接口或Cookie中获取。动态调试是验证猜想的不二法门。
4. Python完整代码实现
在完全理解算法后,我们就可以用Python来复现了。这里提供两种实现方式。
4.1 方式一:纯Python实现(推荐)
如果我们分析出算法是标准的MD5、SHA256或HMAC,Python标准库
hashlib
和
hmac
就能完美应对。
import hashlib
import time
import requests
import json
from urllib.parse import urlencode
def generate_sign(params, secret_key):
"""
生成请求签名
:param params: 字典,请求参数(不包含sign本身)
:param secret_key: 字符串,从JS中分析得到的密钥
:return: 计算得到的sign字符串
"""
# 1. 对参数键进行排序
sorted_params = sorted(params.items(), key=lambda x: x[0])
# 2. 拼接成 key1=value1&key2=value2 格式
param_str = '&'.join([f'{k}={v}' for k, v in sorted_params])
# 3. 获取当前时间戳(毫秒级,与JS的Date.getTime()一致)
timestamp = int(time.time() * 1000)
# 4. 拼接待加密字符串(根据实际分析的格式调整)
# 例如: param_str + "×tamp=" + str(timestamp) + "&secret=" + secret_key
string_to_sign = f"{param_str}×tamp={timestamp}&secret={secret_key}"
# 5. 计算MD5(根据实际算法调整,可能是hashlib.sha256等)
sign = hashlib.md5(string_to_sign.encode('utf-8')).hexdigest()
# 有些网站会再将MD5结果转为大写
# sign = sign.upper()
return sign, timestamp
def fetch_fund_data():
"""
模拟请求私募排排网基金列表数据
"""
# 假设的分析结果
secret_key = "THIS_IS_A_SECRET_KEY" # !!! 这个需要你从JS代码里找出来 !!!
# 构造请求参数(根据实际接口调整)
query_params = {
'page': 1,
'size': 20,
'strategy': '股票策略',
# … 其他参数
}
# 生成签名和时间戳
sign, timestamp = generate_sign(query_params, secret_key)
# 将签名和时间戳加入最终请求参数
final_params = query_params.copy()
final_params['sign'] = sign
final_params['timestamp'] = timestamp
# 目标API地址(需要从Network面板复制)
api_url = "https://api.simuwang.com/YourTargetPath" # 示例地址,请替换
# 设置请求头,模拟浏览器
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Referer': 'https://www.simuwang.com/', # 来源页,有时需要
'Content-Type': 'application/json', # 根据接口实际情况调整
}
try:
# 发送GET请求(如果是POST,则使用requests.post,并将参数放入data或json字段)
response = requests.get(
api_url,
params=final_params, # GET请求参数放在params
headers=headers,
timeout=10
)
response.raise_for_status() # 检查请求是否成功
data = response.json() # 解析JSON响应
print("请求成功,数据示例:")
print(json.dumps(data, indent=2, ensure_ascii=False))
return data
except requests.exceptions.RequestException as e:
print(f"请求出错: {e}")
print(f"响应状态码: {response.status_code if 'response' in locals() else 'N/A'}")
print(f"响应文本: {response.text if 'response' in locals() else 'N/A'}")
except json.JSONDecodeError as e:
print(f"JSON解析出错: {e}")
print(f"原始响应: {response.text}")
if __name__ == '__main__':
fetch_fund_data()
4.2 方式二:使用execjs调用JS代码(适用于复杂或混淆严重的算法)
当加密算法涉及复杂的JS库(如CryptoJS的特定配置)或混淆逻辑难以直接翻译时,直接调用原JS函数是最快最准的。
提取关键JS代码
:从Sources面板,找到包含
generateSign
函数及其所有依赖函数(比如上面的函数
a
,以及
a
函数可能依赖的CryptoJS等)的代码块。将其复制到一个文本文件中,例如
crypto_logic.js
。
封装为可调用模块
:在JS文件末尾,添加导出语句,方便Node.js或execjs调用。
// crypto_logic.js
// … 你复制过来的所有加密相关JS代码 …
// 假设原函数叫 window.generateSign
function generateSign(params, secret) {
// … 原函数体 …
}
// 导出函数,使其可在其他上下文中被调用
module.exports = {
generateSign: generateSign
};
Python调用
:
import execjs
import requests
import json
# 1. 读取JS文件
with open('crypto_logic.js', 'r', encoding='utf-8') as f:
js_code = f.read()
# 2. 编译JS代码
ctx = execjs.compile(js_code)
def fetch_with_js_sign():
secret_key = "THIS_IS_A_SECRET_KEY"
query_params = {
'page': 1,
'size': 20,
'strategy': '股票策略',
}
# 3. 调用JS函数生成签名
# 注意:execjs调用时,参数需要传递给JS函数
sign_result = ctx.call('generateSign', query_params, secret_key)
# 假设JS函数返回一个包含sign和timestamp的对象
sign = sign_result['sign']
timestamp = sign_result['timestamp']
final_params = query_params.copy()
final_params.update({'sign': sign, 'timestamp': timestamp})
api_url = "https://api.simuwang.com/YourTargetPath"
headers = {
'User-Agent': 'Mozilla/5.0 …',
}
response = requests.get(api_url, params=final_params, headers=headers)
data = response.json()
print(json.dumps(data, indent=2, ensure_ascii=False))
return data
if __name__ == '__main__':
fetch_with_js_sign()
注意事项:使用
execjs
性能不如纯Python,且依赖Node.js环境。在部署到服务器时,需要确保环境一致。但对于快速验证和破解复杂加密,它是利器。
5. 常见问题与排查技巧实录
在实际操作中,你几乎一定会遇到下面这些问题。这里是我的排查实录。
5.1 问题一:生成的签名总是无效,服务器返回签名错误
-
可能原因1:参数排序规则不一致
。JS的
Object.keys().sort()
和Python的
sorted(dict.items())
默认都是按字符串Unicode码点排序,理论上一致。但要注意,如果参数值本身是嵌套对象或数组,两者的序列化方式可能不同。
解决方案
:确保拼接前的参数字符串完全一致。可以将JS中待加密的字符串和Python中拼接的字符串都打印出来,进行逐字符比对。
-
可能原因2:空格或特殊字符编码问题
。JS和Python对空格、加号
+
、中文等字符的URL编码处理可能不同。
解决方案
:在拼接前,确保所有参数值都经过统一的URL编码(或都不编码,严格按照JS中的原始值处理)。使用
urllib.parse.quote
但要小心,有时JS用的是
encodeURIComponent
,两者有细微差别。
-
可能原因3:时间戳格式或精度不对
。JS的
Date.getTime()
是毫秒级(13位),Python的
time.time()
是秒级浮点数(10位小数)。
解决方案
:使用
int(time.time() * 1000)
来获取13位毫秒时间戳。
-
可能原因4:密钥(Secret)不对或过期
。密钥可能不是硬编码,而是从其他接口动态获取,或者有有效期。
解决方案
:动态调试时,检查传入
generateSign
函数的第二个参数
e
的值,确认其来源。可能需要先请求一个获取token的接口。
5.2 问题二:断点打不上,或者代码被混淆得无法阅读
-
解决方案
:使用
“XHR/fetch Breakpoints”
全局断点,这是最可靠的方法。对于混淆代码,不要试图完全读懂,关注
变量值的变化
。在关键函数入口和出口打上断点,观察输入和输出。可以借助浏览器的
“Pretty print”
按钮(
{}
图标)格式化混淆的代码,使其有一定可读性。
5.3 问题三:算法中使用了浏览器特有的对象或API(如
window.btoa
,
Crypto.subtle
)
-
解决方案
:
-
对于
btoa
(base64编码)、
atob
(解码),Python可以用
base64.b64encode
和
b64decode
替代,但要注意字符串到字节的转换。
-
对于
Crypto.subtle
(Web Crypto API),这是一个较新的标准,在Node.js或普通JS引擎中可能不可用。如果算法依赖于此,
execjs
方案可能失效。此时需要寻找该API的Polyfill(垫片)库,或者更深入地分析其实现的算法(如AES-GCM),然后用Python的
cryptography
库来实现。
-
对于
5.4 问题四:请求被风控,返回403或验证码
-
解决方案
:单纯的签名破解可能不足以绕过高级风控。你需要让你的Python请求看起来更像一个真实的浏览器会话。
-
携带完整的Headers
:复制浏览器请求中的所有Headers,特别是
Cookie
,
User-Agent
,
Referer
,
Accept-Language
等。
-
维持会话
:使用
requests.Session()
对象,它会自动处理Cookies,保持登录态。
-
模拟更真实的行为
:在连续请求间添加随机延时,模拟人的操作间隔。
-
处理动态Cookie
:有些网站的Cookie中也有加密参数,可能需要先访问首页或登录页获取。
-
import time
import random
session = requests.Session()
# 首先,可能要先访问一下首页,获取初始Cookie
session.get('https://www.simuwang.com/', headers=headers)
# 后续请求都使用同一个session
response = session.get(api_url, params=final_params, headers=headers)
# 请求间随机延时
time.sleep(random.uniform(1, 3))
逆向分析JS加密是一个需要耐心、观察力和逻辑推理的过程。它没有一成不变的公式,但核心思路——定位、分析、模拟——是通用的。私募排排网的这个案例,涵盖了最常见的签名生成场景。当你成功跑通代码,拿到数据的那一刻,成就感是非常足的。记住,逆向的目的是为了学习与自动化,务必遵守网站的
robots.txt
协议,合理控制请求频率,不要对目标服务器造成压力。




