1. 项目概述:从一次翻译请求说起
最近在整理一些外文资料,需要批量处理,自然就想到了调用翻译接口。市面上公开的API要么有额度限制,要么就是收费的。于是,我像很多开发者一样,把目光投向了那些提供免费网页翻译服务的大厂,比如有道翻译。直接打开网页,输入文本,点击翻译,一切看起来都很顺畅。但当我们想用程序自动化这个流程时,问题就来了:直接模拟一个POST请求,把文本发过去,返回的往往是错误页面或者风控拦截。
这就是典型的 前端加密 场景。为了反爬虫和保障服务安全,网站会在浏览器端(即JavaScript执行环境)对请求参数进行加密或签名,使得简单的请求复制失效。要突破这层障碍,就必须深入浏览器内部,搞清楚JavaScript代码到底对我们的输入做了什么手脚。这个过程,就是我们常说的 JS逆向 。今天,我就以“某道翻译”这个非常经典且适合练手的案例,带你完整走一遍JS逆向的分析、定位、还原与复现流程。无论你是刚接触爬虫的新手,还是想巩固逆向思路的老兵,这个案例都能让你收获颇丰。
2. 逆向目标分析与环境准备
在动手之前,我们必须明确目标。我们的核心目标是: 模拟有道翻译网页版(fanyi.youdao.com)的翻译请求,成功获取翻译结果 。
2.1 核心需求拆解
要实现这个目标,我们需要解决几个具体问题:
- 找到真正的翻译接口 :翻译动作触发的网络请求地址是什么?
- 识别关键加密参数 :这个接口需要哪些参数?其中哪些是动态生成、被加密的?
- 定位加密函数 :在庞大的JS代码库中,找到生成这些加密参数的JavaScript函数。
- 理解加密逻辑 :分析该函数的输入、输出和内部算法。
- 本地复现加密 :使用Python(或其他语言)重写加密逻辑,生成合法的参数。
- 组装请求并获取数据 :使用复现的加密算法生成参数,模拟浏览器发送请求,解析返回的翻译结果。
2.2 工具选型与配置
工欲善其事,必先利其器。以下是本次逆向分析的核心工具栈,它们构成了我们的“数字侦探套装”。
浏览器开发者工具(Chrome DevTools) :这是我们的主战场。重点关注以下几个面板:
- Network(网络) :用于捕获和分析所有网络请求,这是找到接口和查看参数的起点。
- Sources(源代码) :用于查看、搜索和调试JavaScript源代码。
- Console(控制台) :用于执行JavaScript代码片段,测试我们的猜想。
- Application(应用) :可以查看Cookie、LocalStorage等本地存储数据。
Node.js环境 :虽然最终用Python实现,但在分析阶段,Node.js环境能让我们非常方便地执行和测试剥离出来的JS代码片段,验证其功能是否正常,这比在浏览器控制台调试大段代码更高效。
Python环境及相关库 :最终实现自动化的地方。
-
requests:用于发送HTTP请求。 -
execjs或PyExecJS:一个非常关键的库,它允许我们在Python中调用JavaScript代码引擎(如Node.js)。当我们无法或不方便将复杂的JS加密逻辑完全用Python重写时,可以直接“搬运”关键的JS函数,在Python中调用它来生成加密参数。这是JS逆向中一种常见且实用的策略。 -
(可选)
json,time,random等标准库,用于数据处理。
注意 :在开始分析前,请确保你的浏览器处于“无痕模式”或清空了有道翻译站点的Cookie和本地存储。这可以避免一些基于用户登录状态的复杂参数干扰我们的分析,让我们从“纯净”的首次访问开始。
3. 网络抓包与接口定位
一切准备就绪,我们打开Chrome的无痕窗口,访问
fanyi.youdao.com
。
3.1 捕获翻译请求
- 打开开发者工具(F12),切换到 Network(网络) 面板。
- 确保录制按钮是红色开启状态,并勾选上 “Preserve log”(保留日志) ,防止页面跳转时请求记录被清空。
- 在翻译页面的输入框中,输入一个测试文本,例如 “hello world”,然后点击“翻译”按钮。
- 此时,Network面板会瞬间出现一系列新的请求。我们需要从中找到那个携带了我们翻译文本和结果的“关键请求”。
3.2 识别关键接口
在一众请求中(可能包括
.js
,
.css
, 图片资源等),我们需要寻找一个看起来像是提交数据的请求。通常它具有以下特征:
-
请求方法
:
POST -
请求URL
:包含
translate或api等关键字。 -
请求负载(Payload)
:
Form Data或Request Payload中包含我们输入的文本 “hello world”。
经过筛选,我们很容易找到一个名为
translate_o
的POST请求,其URL类似于
https://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule
。点击这个请求,查看其
Headers
和
Payload
。
在
Payload
标签页下,选择
Form Data
视图,我们会看到一堆参数。其中
i: hello world
就是我们的输入文本。除此之外,还有一堆令人眼花缭乱的参数,例如:
salt: 17290612345678
sign: 8c0c5b7a9f3d1e2a4b6c8d0f2e4a6c8d
lts: 1729061234567
bv: 5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d
...
这里的
salt
,
sign
,
lts
,
bv
等,就是我们需要攻克的“加密参数”。它们每次请求都会变化,无法使用固定值。我们的核心任务就是找出
sign
和
bv
等参数的生成规律。
实操心得 :
translate_o这个接口名中的_o很有意思,它很可能代表 “original” 或 “official”。在一些历史版本中,可能存在不带_o的接口。网站通过更换接口地址和参数名来增加逆向难度,是一种常见的反爬策略。因此,抓包时一定要认准当前时刻实际发出请求的接口地址,不要死记硬背教程里的旧地址。
4. 加密参数分析与JS代码定位
现在我们知道关键参数是
sign
和
bv
。如何找到生成它们的代码呢?
4.1 搜索与断点调试
-
全局搜索
:在开发者工具的
Sources
面板,按
Ctrl+Shift+F(Windows) 或Cmd+Opt+F(Mac) 打开全局搜索。尝试搜索这些参数名,如sign:、salt:、bv:。更有效的方法是搜索这些参数名的一部分,或者搜索可能赋值的关键字,如sign =、MD5(因为sign看起来像MD5哈希值)。 -
XHR断点
:在
Sources
面板,找到 “XHR/fetch Breakpoints” 区域,点击 “+” 号,添加一个包含部分接口URL的断点,例如
translate_o。然后重新触发翻译动作。浏览器会在发起这个XHR请求前暂停,此时调用栈(Call Stack)会显示导致这个请求的所有JavaScript函数,我们可以顺着调用栈一步步往回找,找到设置参数的地方。 -
Hook技巧
:这是一种更高级的方法。在Console中执行特定的JavaScript代码,来“钩住”像
JSON.stringify、Date.now、Math.random或者encodeURIComponent这样的常用函数。当这些函数被调用时,就会触发断点并打印出调用栈,从而快速定位到加密逻辑的入口。对于初学者,从搜索和XHR断点开始更直观。
4.2 定位核心加密函数
通过上述方法(通常搜索
sign:
就能直接定位),我们可以在一个被压缩的JS文件(如
fanyi.min.js
或
webTranslate.min.js
)中找到关键代码段。压缩的代码没有换行和空格,所有变量名都是单字母,可读性极差。
我们需要利用开发者工具的
“Pretty print”
功能(那个
{}
图标)来格式化代码。格式化后,代码结构会清晰很多。
在格式化后的代码中搜索,我们可能会找到类似这样的结构:
var r = function(e) {
var t = n.md5(navigator.appVersion),
r = "" + (new Date).getTime(),
i = r + parseInt(10 * Math.random(), 10);
return {
ts: r,
bv: t,
salt: i,
sign: n.md5("fanyideskweb" + e + i + "Ygy_4c=r#e#4EX^NUGUc5")
}
};
这段代码虽然变量名可能不同(
r
,
t
,
i
),但逻辑非常典型:
-
t/bv: 来自n.md5(navigator.appVersion),即对浏览器版本信息求MD5。 -
r/ts/lts:(new Date).getTime(),当前时间戳。 -
i/salt: 时间戳 + 一个随机整数。 -
sign: 对字符串"fanyideskweb" + 待翻译文本 + salt + 一个固定密钥求MD5。
核心原理解析 :这种设计是一种简单的 签名验证 。服务器端同样知道这个算法和固定密钥(
Ygy_4c=r#e#4EX^NUGUc5,这个值需要从代码中提取,它可能变化)。客户端生成sign随请求发送,服务器收到后,用自己的密钥按同样算法计算一遍sign。如果两者一致,则认为请求是合法的、未被篡改的。salt(盐值)和ts(时间戳)的引入,确保了每次请求的sign都不同,防止了简单的重放攻击。
4.3 验证加密逻辑
定位到疑似函数后,不要急于开始用Python重写。先在浏览器的 Console 里进行验证。
-
将疑似函数代码复制到Console中(可能需要补全它依赖的
n.md5函数,通常n是一个包含MD5方法的工具对象,可以从源代码里找到并一并复制)。 -
定义一个变量
e为"hello world",然后调用这个函数r(e)。 -
观察输出对象,对比其
sign、salt、bv的值,是否与刚才Network抓包中看到的参数值 形态一致 (例如,sign是否是32位十六进制字符串)。 -
重新触发一次翻译,抓取新的参数,然后用同样的输入
"hello world"在Console里再执行一次函数。你会发现,因为salt中的随机数和ts的变化,两次生成的sign完全不同,但 算法模式 应该是一样的。
通过Console验证,是确保我们找对函数的 关键一步 ,能避免后续很多徒劳的努力。
5. 加密算法本地复现(Python实现)
确认了加密函数,接下来就是要在我们的Python脚本中复现这个逻辑。有两种主流策略:
5.1 策略一:纯Python重写(推荐,便于理解)
如果加密逻辑不复杂(如本例的MD5),强烈建议用Python重写。这有助于彻底理解算法,且执行效率高,依赖少。
首先,我们需要从JS代码中提取出关键信息:
-
固定密钥(secretKey)
:本例中是
"Ygy_4c=r#e#4EX^NUGUc5"。 -
bv的生成方式 :MD5(navigator.appVersion)。navigator.appVersion是浏览器用户代理的一部分,我们可以用一个固定的、合理的字符串来模拟,例如"5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"。在实际爬虫中,为了更仿真,可以使用fake_useragent库生成一个,然后取它的MD5。但经过测试,有道翻译对这个值的校验并不严格,一个固定的、符合格式的MD5值通常也能工作。 -
salt和sign的生成算法 :如上所述。
下面是用Python实现的代码:
import hashlib
import time
import random
def youdao_sign(text, app_version_ua):
"""
生成有道翻译的签名参数
:param text: 待翻译文本
:param app_version_ua: 模拟的浏览器UA版本字符串
:return: 包含 ts, bv, salt, sign 的字典
"""
# 1. 生成 lts (ts) 时间戳(毫秒级,13位)
lts = str(int(time.time() * 1000))
# 2. 生成 salt: lts + 一位随机数
salt = lts + str(random.randint(0, 9))
# 3. 生成 bv: 对浏览器UA版本信息求MD5
bv = hashlib.md5(app_version_ua.encode('utf-8')).hexdigest()
# 4. 生成 sign: md5(“fanyideskweb” + text + salt + 固定密钥)
# 注意:这里的固定密钥需要从JS代码中准确提取
secret_key = "Ygy_4c=r#e#4EX^NUGUc5" # 示例密钥,请以实际JS代码为准
sign_str = f"fanyideskweb{text}{salt}{secret_key}"
sign = hashlib.md5(sign_str.encode('utf-8')).hexdigest()
return {
'ts': lts,
'bv': bv,
'salt': salt,
'sign': sign
}
# 测试
if __name__ == '__main__':
test_text = "hello world"
fake_ua = "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
params = youdao_sign(test_text, fake_ua)
print(params)
5.2 策略二:使用 execjs 调用JS代码(处理复杂加密)
如果加密算法异常复杂(例如涉及大量的浏览器环境对象、自定义的加密库、复杂的位运算等),完全用Python重写成本过高或容易出错。这时,我们可以将关键的JS函数代码“抠”出来,保存为一个
.js
文件,然后在Python中用
execjs
来调用它。
步骤:
-
创建一个
youdao_encrypt.js文件,将我们找到的完整加密函数及其依赖(比如那个n.md5的实现,或者整个CryptoJS库的片段)放入其中,并导出函数。// youdao_encrypt.js // 这里可能需要引入或定义MD5函数,例如使用常见的 md5 库 const crypto = require('crypto'); function md5(text) { return crypto.createHash('md5').update(text).digest('hex'); } function generateSign(text, appVersion) { const lts = Date.now().toString(); const salt = lts + Math.floor(Math.random() * 10); const bv = md5(appVersion); const secretKey = "Ygy_4c=r#e#4EX^NUGUc5"; // 实际密钥 const signStr = `fanyideskweb${text}${salt}${secretKey}`; const sign = md5(signStr); return { lts, salt, bv, sign }; } // 导出函数供Node.js环境或execjs调用 if (typeof module !== 'undefined' && module.exports) { module.exports = generateSign; } -
在Python中使用
execjs调用。import execjs import os # 读取JS文件 with open('youdao_encrypt.js', 'r', encoding='utf-8') as f: js_code = f.read() # 创建JS执行环境 ctx = execjs.compile(js_code) # 调用JS函数 text = "hello world" app_version = "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" result = ctx.call('generateSign', text, app_version) print(result)
注意事项 :
execjs的性能通常不如纯Python,且依赖本地的JavaScript运行时(如Node.js)。它更适合作为原型验证或处理极端复杂逻辑的过渡方案。对于有道翻译这个案例,纯Python重写是更优解。
6. 构建完整请求与数据处理
有了生成加密参数的能力,我们就可以组装完整的请求了。
6.1 组装请求参数
查看Network中
translate_o
请求的
Form Data
,我们需要把所有参数都凑齐。除了加密生成的
salt
,
sign
,
lts
,
bv
,还有一些固定或简单的参数:
-
i: 待翻译文本。 -
from: 源语言,如AUTO。 -
to: 目标语言,如AUTO。 -
smartresult:dict。 -
client:fanyideskweb(固定)。 -
doctype:json(固定)。 -
version:2.1(固定)。 -
keyfrom:fanyi.web(固定)。 -
action:FY_BY_REALTlME(可能是固定或根据某些规则生成,抓包观察)。
我们需要构建一个字典,包含所有这些参数。
6.2 发送请求与解析结果
使用
requests
库发送POST请求。
务必注意请求头(Headers)的模拟
,特别是
Content-Type
、
Referer
和
User-Agent
。直接从浏览器复制这些头信息是最稳妥的。
import requests
import json
import time
import random
import hashlib
def translate_youdao(text, from_lang='AUTO', to_lang='AUTO'):
"""
调用有道翻译网页版接口
"""
url = "https://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule"
# 1. 生成加密参数
ua_for_bv = "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
lts = str(int(time.time() * 1000))
salt = lts + str(random.randint(0, 9))
bv = hashlib.md5(ua_for_bv.encode()).hexdigest()
secret_key = "Ygy_4c=r#e#4EX^NUGUc5" # 请替换为实际密钥
sign_str = f"fanyideskweb{text}{salt}{secret_key}"
sign = hashlib.md5(sign_str.encode()).hexdigest()
# 2. 组装表单数据
data = {
'i': text,
'from': from_lang,
'to': to_lang,
'smartresult': 'dict',
'client': 'fanyideskweb',
'salt': salt,
'sign': sign,
'lts': lts,
'bv': bv,
'doctype': 'json',
'version': '2.1',
'keyfrom': 'fanyi.web',
'action': 'FY_BY_REALTlME', # 注意这里是 `l` 不是 `1`
}
# 3. 设置请求头(从浏览器复制)
headers = {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Host': 'fanyi.youdao.com',
'Origin': 'https://fanyi.youdao.com',
'Referer': 'https://fanyi.youdao.com/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest',
# Cookie 可能不是必须的,但如果请求失败,可以尝试从浏览器复制一个有效的Cookie过来
# 'Cookie': '你的Cookie'
}
# 4. 发送请求
try:
resp = requests.post(url, data=data, headers=headers, timeout=10)
resp.raise_for_status() # 检查HTTP错误
result_json = resp.json()
# 5. 解析结果
# 有道翻译的返回结构相对复杂,主要结果在 `translateResult` 中
if 'translateResult' in result_json:
# translateResult 是一个三维数组 [[{“src”:”...”,”tgt”:”...”}]]
translation = result_json['translateResult'][0][0]['tgt']
return translation
else:
print(f"响应中未找到翻译结果: {result_json}")
return None
except requests.exceptions.RequestException as e:
print(f"网络请求失败: {e}")
return None
except json.JSONDecodeError as e:
print(f"响应解析失败: {e}, 原始响应: {resp.text[:200]}")
return None
# 测试
if __name__ == '__main__':
result = translate_youdao("hello world")
print(f"翻译结果: {result}")
6.3 处理反爬机制
即使参数正确,服务器也可能返回错误码,如
50
。这通常意味着触发了反爬机制。可以尝试以下策略:
- 添加Cookies :从浏览器复制一组有效的Cookie到头信息中。
-
使用Session
:使用
requests.Session()对象,它会自动管理Cookie,模拟浏览器会话。 -
降低请求频率
:在请求间增加随机延时
time.sleep(random.uniform(1, 3))。 -
更换User-Agent
:使用
fake_useragent库随机生成。 - 使用代理IP :如果IP被限制,需要轮换代理。
踩坑记录 :有道翻译的
action参数值FY_BY_REALTlME中的倒数第三个字母是 小写的L ,不是数字1。如果写错,签名验证会失败。这种细节必须从抓包数据中精确复制。
7. 常见问题排查与优化技巧
在实际操作中,你可能会遇到各种问题。下面是一个快速排查指南:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
返回错误码
50
| 签名验证失败,或触发了风控。 |
1.
检查签名算法
:确认
secret_key
、拼接顺序 (
fanyideskweb{text}{salt}{key}
) 是否与JS代码完全一致。
2. 检查
bv
:确认生成
bv
的UA字符串是否合理,MD5计算是否正确。
3. 检查
salt
:确认
salt
是
lts
拼接一个0-9的随机数。
4. 添加完整请求头 :特别是
Referer
,
Origin
,
Cookie
。
5. 降低请求频率 。 |
返回
{"errorCode":50}
但参数看似正确
|
时间戳
lts
或
salt
的格式或值有问题。
|
1. 确保
lts
是
13位
的毫秒时间戳字符串。
2. 确保
salt
是
lts
后面
直接拼接
一位随机数,没有空格或其他字符。
3. 对比浏览器Console中生成的
lts
、
salt
和你代码生成的是否格式一致。
|
| 请求被重定向或返回非JSON内容 | 请求头不完整,被服务器识别为非法请求。 |
1.
务必添加
Referer
和
Origin
头,值应为
https://fanyi.youdao.com/
。
2. 添加
X-Requested-With: XMLHttpRequest
头,表明是Ajax请求。
3. 使用Session对象 保持会话状态。 |
execjs
报错,提示某些对象未定义
| 抠出来的JS代码依赖浏览器或Node.js特有的全局对象。 |
1. 在JS文件开头模拟这些环境,例如
window = global;
(在Node.js中) 或定义
navigator = { appVersion: '...' }
。
2. 尽量将依赖的JS库(如MD5函数)的代码完整地抠出来,一起放入文件。 |
| 翻译长文本失败 | 可能有长度限制,或长文本的加密处理有细微差别。 |
1. 检查Network中成功翻译长文本时的请求参数,与你代码生成的进行对比。
2. 考虑将长文本分段翻译。 |
优化技巧:
-
密钥动态获取
:更健壮的做法不是将
secret_key硬编码在代码里,而是写一个辅助函数,定期从网页的JS代码中通过正则表达式提取它。虽然这个密钥不常变,但这样做程序适应性更强。 -
使用Session管理状态
:始终使用
requests.Session(),它自动处理Cookies,使多次请求更像同一个浏览器会话。 -
结构化返回结果
:有道翻译的返回数据里除了
translateResult,还有smartResult包含词典释义等。可以完善解析函数,返回一个包含翻译结果、音标、释义等信息的结构化字典。 - 异常重试机制 :对于网络错误或偶尔的风控拦截,可以加入简单的重试逻辑。
这个“某道翻译逆向”案例,几乎包含了JS逆向入门所需的所有核心步骤:抓包、定位、分析、还原、复现、调优。它使用的MD5算法和参数拼接方式,是Web逆向中最常见的形式之一。掌握这个案例,你就拿到了打开许多类似网站前端加密大门的第一把钥匙。逆向工程没有一成不变的套路,核心是耐心观察、大胆假设、小心验证。每一次成功的逆向,都是对开发者思维的一次绝佳训练。

1601

被折叠的 条评论
为什么被折叠?



