1. 项目概述:当爬虫遇上JS加密与签名验证
十年前我刚入行爬虫时,大多数网站还停留在简单的robots.txt防护阶段。如今随便打开一个电商页面,都能看到层层加密防护:接口参数被国密4加密、请求头带着动态签名、关键数据用RSA非对称加密传输。上周我尝试爬取某旅游平台数据时,甚至遇到了迭代加密三角网这种新型防御手段。
这次要破解的正是典型的"JS逆向+签名算法"双重防护体系。目标网站不仅用JavaScript混淆了核心加密逻辑,还对每个请求都添加了动态生成的签名参数。常规的requests直接请求会立即触发风控,返回403状态码。通过Chrome开发者工具抓包分析,发现关键难点在于:
- 登录接口的密码字段经过RSA加密
- 查询接口携带由时间戳、设备指纹生成的32位签名
- 数据返回体采用自定义的字节流编码方式
2. 核心防护机制逆向分析
2.1 JavaScript加密逻辑定位
首先在Chrome的Sources面板对目标网站进行调试。通过搜索关键词"encrypt"、"sign"等,很快在bundle.js中发现可疑代码段:
function generateSign(timestamp, deviceId) { var key = CryptoJS.enc.Utf8.parse("7a3265f1b2e4c8d9"); var iv = CryptoJS.enc.Utf8.parse("9e8d7c6b5a43210f"); var srcStr = deviceId + "|" + timestamp; return CryptoJS.AES.encrypt(srcStr, key, {iv: iv}).toString(); }这段代码揭示了三层防护:
- 使用CryptoJS库实现AES-CBC加密
- 密钥和IV都是硬编码在JS中的固定值
- 签名原料由设备ID和时间戳通过管道符连接
实战技巧:现代网站常会通过Webpack将代码拆分成多个chunk,此时需要先在Network面板找到包含加密逻辑的JS文件。可以尝试搜索
encrypt、CryptoJS、sign等关键词快速定位。
2.2 Python还原加密过程
对应的Python实现需要安装pycryptodome库:
from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 def generate_sign(timestamp, device_id="default_device"): key = b"7a3265f1b2e4c8d9" iv = b"9e8d7c6b5a43210f" cipher = AES.new(key, AES.MODE_CBC, iv) plaintext = f"{device_id}|{timestamp}".encode() ciphertext = cipher.encrypt(pad(plaintext, AES.block_size)) return base64.b64encode(ciphertext).decode()测试发现这个签名算法存在两个漏洞:
- 设备ID可以使用固定值
- 没有校验时间戳的合法性
3. 完整请求链路破解
3.1 请求参数加密处理
目标网站的搜索接口要求对查询参数进行字段排序和MD5签名:
import hashlib def params_sign(params): sorted_str = '&'.join([f"{k}={params[k]}" for k in sorted(params)]) return hashlib.md5(sorted_str.encode()).hexdigest() # 使用示例 params = { "keyword": "手机", "page": 1, "size": 20 } sign = params_sign(params) # 输出:3e4a5b6c7d8e9f0a1b2c3d4e5f6g7h83.2 请求头动态生成
通过分析正常请求,发现需要携带以下特殊头信息:
headers = { "X-Request-Sign": generate_sign(int(time.time())), "X-Device-Id": "a1b2c3d4e5f6", "X-Timestamp": str(int(time.time() * 1000)), "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" }4. 反反爬策略深度优化
4.1 指纹对抗方案
现代风控系统会检测浏览器指纹特征。我们使用selenium-wire配合以下配置:
from seleniumwire import webdriver options = { 'disable_encoding': True, 'ignore_http_methods': ['OPTIONS'] } driver = webdriver.Chrome(seleniumwire_options=options) # 修改WebGL指纹 driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { "source": """ const getParameter = WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter = function(parameter) { if (parameter === 37445) return 'Intel Open Source Technology Center'; if (parameter === 37446) return 'Mesa DRI Intel(R) HD Graphics 520'; return getParameter(parameter); }; """ })4.2 请求频率控制策略
采用自适应延迟算法防止被封:
import random import time class RequestThrottle: def __init__(self, base_delay=1.0): self.base_delay = base_delay self.error_count = 0 def wait(self): delay = self.base_delay * (1 + self.error_count * 0.5) jitter = random.uniform(-0.2, 0.2) time.sleep(max(0.5, delay + jitter)) def record_error(self): self.error_count = min(self.error_count + 1, 5) def record_success(self): self.error_count = max(self.error_count - 1, 0)5. 数据解密处理
5.1 响应体解密案例
当遇到类似"纵向加密"的数据时,可以尝试以下处理流程:
def decrypt_response(ciphertext): # 第一步:Base64解码 raw_data = base64.b64decode(ciphertext) # 第二步:字节流处理 magic_num = raw_data[:4] # 文件头标识 version = raw_data[4:6] # 版本号 payload = raw_data[6:] # 有效载荷 # 第三步:异或解密 key = 0xAB decrypted = bytes([b ^ key for b in payload]) # 第四步:JSON解析 return json.loads(decrypted.decode())5.2 数据清洗技巧
对于HTML加密混淆的情况,推荐使用以下处理链:
- 先用正则提取有效数据段
- 使用html.unescape处理HTML实体
- 用bs4清理残留标签
- 最后进行unicode标准化
import re from bs4 import BeautifulSoup import html def clean_html(html_str): # 提取加密数据块 encrypted = re.search(r'<script id="__DATA__">(.*?)</script>', html_str).group(1) # 多层解码 decoded = html.unescape(encrypted) decoded = bytes.fromhex(decoded[2:]).decode('utf-8') # 最终清理 soup = BeautifulSoup(decoded, 'html.parser') return soup.get_text()6. 工程化建议
6.1 代理IP管理方案
建议使用以下架构管理代理IP:
class ProxyManager: def __init__(self, proxy_list): self.proxies = proxy_list self.current = 0 self.blacklist = set() def get_proxy(self): while True: proxy = self.proxies[self.current] self.current = (self.current + 1) % len(self.proxies) if proxy not in self.blacklist: return proxy def ban_proxy(self, proxy): self.blacklist.add(proxy) if len(self.blacklist) > len(self.proxies) / 2: self.blacklist.clear()6.2 日志监控体系
完善的日志系统应该包含:
import logging from logging.handlers import RotatingFileHandler def setup_logger(name): logger = logging.getLogger(name) logger.setLevel(logging.DEBUG) # 文件日志(按100MB轮转) file_handler = RotatingFileHandler( 'spider.log', maxBytes=100*1024*1024, backupCount=5 ) file_formatter = logging.Formatter( '%(asctime)s - %(levelname)s - %(message)s' ) file_handler.setFormatter(file_formatter) # 控制台日志 console_handler = logging.StreamHandler() console_formatter = logging.Formatter( '[%(levelname)s] %(message)s' ) console_handler.setFormatter(console_formatter) logger.addHandler(file_handler) logger.addHandler(console_handler) return logger7. 法律与伦理边界
在爬虫开发中必须注意:
- 严格遵守robots.txt协议
- 单机请求频率控制在人类操作范围内
- 不爬取个人隐私数据
- 不使用爬虫进行商业竞争或攻击
我曾遇到一个案例:某电商平台通过检测鼠标移动轨迹识别爬虫。合法合规的做法是:
- 通过公开API获取数据
- 购买官方数据服务
- 限制爬取频率在合理范围
重要提示:近期出现多起因爬虫引发的法律纠纷,建议在开发前咨询法律顾问,确保业务模式合规。某些加密算法(如国密)的使用可能受到法律限制,解密此类数据存在法律风险。