
1.1 为什么参数加密如此有效?
参数加密机制的核心在于:服务器通过验证请求参数的完整性和时效性来区分人类用户与机器程序。当您在网页上点击"查询"时,浏览器会执行复杂的JS代码,生成一个或多个经过加密的签名参数。这些参数往往具有:
1.2 12306加密的典型特征
通过对12306网络请求的观察,我们可以发现以下典型特征:
_json_att等字段,每次会话都会变化leftTicketSecret等哈希值2.1 必备工具栈
2.2 关键分析步骤
逆向分析遵循"由外到内"的原则:
让我们以车票查询接口为例,进行完整的逆向分析。
3.1 识别加密参数
首先在浏览器中打开12306车票查询页面,开启Network监控,执行一次查询。观察请求的Query String Parameters,会发现类似以下的参数结构:
javascript
// 原始请求参数
leftTicketDTO: {
train_date: "2024-11-24",
from_station: "BJP",
to_station: "SHH",
purpose_codes: "ADULT"
}
// 实际请求URL(示例)
https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2024-11-24&leftTicketDTO.from_station=BJP&leftTicketDTO.to_station=SHH&purpose_codes=ADULT&_json_att=...注意_json_att这个参数,它就是我们需要破解的加密参数之一。
3.2 定位加密函数
在Chrome DevTools中:
_json_att在所有JS文件中的出现位置我们会发现关键代码通常隐藏在压缩的JS文件中。使用Pretty-print格式化后,可以找到类似这样的代码段:
javascript
// 格式化后的关键JS代码片段
function generateJsonAtt() {
var e = Math.random().toString(36).substr(2);
var t = new Date().getTime();
var n = encryptMethod(e + "_" + t);
return window.btoa(n);
}
function encryptMethod(str) {
// 复杂的加密逻辑,可能涉及多个步骤
var key = CryptoJS.enc.Utf8.parse('1234567812345678');
var iv = CryptoJS.enc.Utf8.parse('1234567812345678');
var encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(str), key, {
keySize: 128 / 8,
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
}3.3 算法分析与代码移植
分析上述代码,我们发现加密流程:
现在我们需要在Python中重现这个逻辑:
python
import execjs
import time
import random
import string
class Zhang12306JSEncrypt:
def __init__(self):
# 编译JS加密代码
with open('12306_encrypt.js', 'r', encoding='utf-8') as f:
js_code = f.read()
self.ctx = execjs.compile(js_code)
def generate_json_att(self):
"""生成_json_att参数"""
return self.ctx.call('generateJsonAtt')
# 对应的JS文件 (12306_encrypt.js)
"""
const CryptoJS = require('crypto-js');
function generateRandomString(length) {
return Math.random().toString(36).substr(2, length);
}
function generateJsonAtt() {
var random_str = generateRandomString(16);
var timestamp = new Date().getTime();
var raw_str = random_str + "_" + timestamp;
var encrypted = encryptMethod(raw_str);
return Buffer.from(encrypted.toString(), 'binary').toString('base64');
}
function encryptMethod(str) {
var key = CryptoJS.enc.Utf8.parse('1234567812345678');
var iv = CryptoJS.enc.Utf8.parse('1234567812345678');
var encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(str), key, {
keySize: 128 / 8,
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}
module.exports = {
generateJsonAtt: generateJsonAtt
};
"""3.4 完整请求示例
python
import requests
from Zhang12306JSEncrypt import Zhang12306JSEncrypt
class Advanced12306Crawler:
def __init__(self):
self.session = requests.Session()
self.encryptor = Zhang12306JSEncrypt()
# 代理配置
self.proxyHost = "www.16yun.cn"
self.proxyPort = "5445"
self.proxyUser = "16QMSOML"
self.proxyPass = "280651"
# 设置请求头
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': 'https://kyfw.12306.cn/otn/leftTicket/init',
})
def get_proxies(self):
"""构造代理配置字典"""
proxy_url = f"http://{self.proxyUser}:{self.proxyPass}@{self.proxyHost}:{self.proxyPort}"
return {
'http': proxy_url,
'https': proxy_url
}
def query_tickets(self, train_date, from_station, to_station):
"""查询车票信息"""
# 生成加密参数
json_att = self.encryptor.generate_json_att()
# 构造请求参数
params = {
'leftTicketDTO.train_date': train_date,
'leftTicketDTO.from_station': from_station,
'leftTicketDTO.to_station': to_station,
'purpose_codes': 'ADULT',
'_json_att': json_att
}
url = 'https://kyfw.12306.cn/otn/leftTicket/query'
try:
# 使用代理发起请求
response = self.session.get(
url,
params=params,
proxies=self.get_proxies(),
timeout=10 # 添加超时设置
)
if response.status_code == 200:
data = response.json()
if data.get('status'):
return self.parse_ticket_data(data['data'])
else:
print("API返回状态错误:", data.get('messages', ['未知错误']))
else:
print(f"HTTP错误: {response.status_code}")
except requests.exceptions.ProxyError as e:
print(f"代理连接错误: {e}")
except requests.exceptions.ConnectTimeout as e:
print(f"连接超时: {e}")
except requests.exceptions.ReadTimeout as e:
print(f"读取超时: {e}")
except Exception as e:
print(f"请求异常: {e}")
return None
def parse_ticket_data(self, data):
"""解析车票数据"""
# 简化的解析逻辑
result = []
for item in data.get('result', []):
info = item.split('|')
if len(info) > 3:
result.append({
'车次': info[3],
'出发站': info[6],
'到达站': info[7],
'出发时间': info[8],
'到达时间': info[9],
'历时': info[10]
})
return result
# 使用示例
if __name__ == "__main__":
crawler = Advanced12306Crawler()
tickets = crawler.query_tickets('2024-11-24', 'BJP', 'SHH')
if tickets:
for ticket in tickets[:3]:
print(ticket)4.1 处理代码混淆与压缩
12306的JS代码经常更新且高度混淆。应对策略:
4.2 自动化更新机制
python
import hashlib
import os
class AutoUpdateEncryptor:
def __init__(self, js_url):
self.js_url = js_url
self.local_file = '12306_encrypt.js'
self.ctx = None
self.load_js()
def get_js_md5(self):
"""计算JS文件MD5以检测变化"""
with open(self.local_file, 'rb') as f:
return hashlib.md5(f.read()).hexdigest()
def download_js(self):
"""下载最新JS文件"""
# 实现下载逻辑
pass
def load_js(self):
"""加载JS执行环境"""
if not os.path.exists(self.local_file):
self.download_js()
with open(self.local_file, 'r', encoding='utf-8') as f:
js_code = f.read()
self.ctx = execjs.compile(js_code)
def check_and_update(self):
"""检查并更新JS文件"""
# 定期检查JS文件是否更新
pass五、 伦理边界与最佳实践5.1 合法合规使用严格遵守robots.txt协议控制请求频率,避免对服务器造成压力仅用于技术学习与研究目的5.2 技术防护措施实现请求失败的重试机制使用IP代理池分散请求建立完整的日志监控系统结论:逆向工程的艺术与科学破解12306的JS加密参数,是一场在技术边界上的精确舞蹈。它既需要扎实的JavaScript语言基础,又需要对加密算法的深刻理解,更需要耐心细致的调试分析能力。通过本文的深度剖析,我们不仅掌握了一套具体的技术方案,更重要的是建立了一种逆向思维的方****法论。在面对任何复杂的反爬虫机制时,我们都能够:冷静分析:从网络请求入手,识别关键参数精准定位:利用开发者工具,追踪调用链路深度还原:分析加密逻辑,重现算法流程工程实现:构建稳定可靠的爬虫系统
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。