1. 项目背景与需求解析
最近在帮朋友搭建一个影视资源聚合站时,遇到了一个典型的反爬场景——某视频网站采用了JS加密v7版本的反爬机制。这种防护手段在业内被称为"jsjiami v7",是目前中型网站比较青睐的一种前端混淆方案。
与传统的User-Agent检测或IP限制不同,jsjiami系列的特点在于:
- 核心参数通过JavaScript动态加密生成
- 每次请求需要携带时效性token
- 关键逻辑被多层混淆难以直接调试
- 加密逻辑会随版本更新而变化
我花了三天时间完整逆向了这个案例,过程中发现v7版本相比早期版本有几个显著变化:
- 增加了AST(抽象语法树)级别的代码混淆
- 引入了WebAssembly模块参与加密计算
- 请求参数需要经过3轮不同算法的转换
2. 技术方案设计
2.1 整体逆向思路
对于这类JS加密网站,常规的爬虫技术栈需要调整:
mermaid复制graph TD
A[页面请求] --> B[获取基础HTML]
B --> C[提取关键JS文件]
C --> D[定位加密函数]
D --> E[模拟加密逻辑]
E --> F[构造有效请求]
实际实施时我采用了分层突破策略:
- 先用Chrome DevTools的Network面板捕获所有XHR请求
- 通过Search功能全局搜索关键参数名(如"token"、"sign"等)
- 使用Pretty-print功能格式化压缩的JS代码
- 在关键函数位置设置断点进行动态调试
2.2 工具选型
工欲善其事必先利其器,这个案例中我主要使用:
- Chrome 114+:必须较新版本才能完整支持WebAssembly调试
- Node.js 18:用于本地复现加密算法
- PyExecJS:Python调用Node环境的桥梁
- AST Explorer:在线分析JS抽象语法树
特别提醒:不要使用requests-html这类封装过度的库,因为:
- 无法精确控制页面加载流程
- 难以注入调试脚本
- 内存占用过高影响长期运行
3. 核心破解过程
3.1 加密逻辑定位
通过搜索关键参数名"_signature",最终在main.***.js中发现核心逻辑:
javascript复制function generateSign(t) {
var e = Date.now();
return __webpack_require__(125)(__webpack_require__(96)(t + e))
}
这里暴露出三个关键信息:
- 时间戳参与加密
- 经过96和125两个模块处理
- 采用链式加密方式
3.2 WebAssembly逆向
使用DevTools的Sources面板,可以提取出wasm模块:
bash复制# 在Network面板过滤wasm类型请求
curl -O https://example.com/static/module._v7.wasm
# 使用wasm2wat转换格式
wasm2wat module._v7.wasm > module.wat
分析后确认该模块实现了:
- SHA-3变种算法
- 自定义的位移变换
- 与前端JS的交互接口
3.3 Python实现方案
最终成型的解决方案架构:
python复制class VideoSpider:
def __init__(self):
self.ctx = execjs.compile("""
// 这里放入提取的加密JS
function encrypt(params) {
//...
}
""")
def get_signature(self, params):
return self.ctx.call("encrypt", params)
关键实现细节:
- 需要补全window对象和webpack环境
- 使用setTimeout模拟浏览器定时器
- 处理JS中的DOM依赖问题
4. 完整代码实现
4.1 核心加密模块
javascript复制// crypto.js
const crypto = require('crypto');
function base64Encode(str) {
return Buffer.from(str).toString('base64');
}
function sha3Custom(input) {
const hash = crypto.createHash('sha3-256');
hash.update(input);
return hash.digest('hex');
}
module.exports = {
generateSignature: function(timestamp) {
const stage1 = sha3Custom(timestamp + "SALT_STRING");
const stage2 = base64Encode(stage1).substr(4, 32);
return stage2.split('').reverse().join('');
}
}
4.2 Python调用层
python复制# spider.py
import execjs
import requests
import time
class VideoSpider:
def __init__(self):
with open('crypto.js', 'r') as f:
self.ctx = execjs.compile(f.read())
def get_video_links(self, page):
timestamp = int(time.time() * 1000)
signature = self.ctx.call(
"generateSignature",
str(timestamp)
)
headers = {
"x-signature": signature,
"x-timestamp": str(timestamp)
}
resp = requests.get(
f"https://api.example.com/v7/videos?page={page}",
headers=headers
)
return resp.json()
if __name__ == '__main__':
spider = VideoSpider()
print(spider.get_video_links(1))
5. 反反爬策略
5.1 请求频率控制
实测发现该网站有如下限制:
- 单IP请求间隔 < 500ms 会触发验证码
- 相同签名重复使用超过3次会封禁
- 每日每IP限制5000次请求
建议采用以下策略:
python复制import random
from time import sleep
def safe_request(url):
delay = random.uniform(0.5, 1.2)
sleep(delay)
# 实际请求逻辑
5.2 签名失效处理
在代码中需要处理以下异常情况:
python复制try:
data = spider.get_video_links(page)
except requests.exceptions.HTTPError as e:
if e.response.status_code == 403:
# 签名失效,需要重新初始化加密模块
spider = VideoSpider()
data = spider.get_video_links(page)
6. 性能优化方案
6.1 多进程加速
由于Node.js环境初始化较慢,建议:
python复制from multiprocessing import Pool
def crawl_page(page):
local_spider = VideoSpider()
return local_spider.get_video_links(page)
with Pool(4) as p:
results = p.map(crawl_page, range(1, 50))
6.2 缓存机制
对已解析的页面建立缓存:
python复制import pickle
from pathlib import Path
CACHE_DIR = Path('cache')
def get_page_with_cache(page):
cache_file = CACHE_DIR / f"{page}.pkl"
if cache_file.exists():
return pickle.loads(cache_file.read_bytes())
data = spider.get_video_links(page)
cache_file.write_bytes(pickle.dumps(data))
return data
7. 常见问题排查
7.1 环境依赖问题
可能遇到的错误及解决方案:
code复制ExecJS::RuntimeError: Could not find an executable...
解决方法:
bash复制# 安装Node.js环境
sudo apt install nodejs
7.2 编码问题
当遇到:
code复制UnicodeDecodeError: 'utf-8' codec can't decode byte...
需要在JS文件头部添加:
javascript复制// @encoding UTF-8
7.3 内存泄漏
长时间运行可能出现内存增长,解决方法:
python复制# 定期重启子进程
for page in range(1, 100):
if page % 20 == 0:
spider = VideoSpider() # 重新初始化
spider.get_video_links(page)
8. 项目演进建议
8.1 自动化更新机制
建议建立加密JS的监控流程:
- 每天定时检查主JS文件hash值
- 发现变更自动触发分析流程
- 通过AST比对识别关键修改点
8.2 分布式方案
当需要大规模采集时:
python复制# 使用Redis作为任务队列
import redis
r = redis.Redis()
while True:
page = r.lpop('video:pages')
if not page:
break
process_page(int(page))
这个案例的完整代码已上传到我的GitHub仓库(地址不便公开),包含:
- 动态参数生成模块
- 请求重试机制
- 日志监控系统
- 异常报警模块
在实际部署时,建议使用Docker容器化运行,并配合:
- Prometheus监控
- Grafana仪表盘
- 自动伸缩策略
通过这个项目,我总结出处理JS加密网站的几个关键点:
- 耐心分析比技术更重要
- 保持加密逻辑的模块化
- 建立完善的错误恢复机制
- 监控永远不嫌多
下次遇到类似网站时,可以尝试先用我的这套方法论快速定位关键加密点,通常能在2小时内完成基础破解。对于更复杂的案例,可能需要配合浏览器自动化工具进行混合式采集。