声明:本文章仅用于学习探讨,请勿用于任何违反法律的用途,如有侵权请联系下架。
有道web翻译请求流程
- 通过app.js声明产品代称keyid与产品固定密钥constSign
- 向https://dict.youdao.com/webtranslate/key请求,获取AES加密的密钥信息(aesKey, aesIv)以及后续请求的密钥(secretKey)
- 向https://dict.youdao.com/webtranslate发送翻译请求
- 通过AES解密翻译内容
看着是不是很简单,但是逆向分析你的头会变大。
第一次请求分析

通过控制台监听网络可以发现,最重要的请求参数就是keyid,sign,mysticTime。
故此,接下来我们首先对keyid进行分析。
keyid是有道产品的代称,在app.js中可以发现还有ai翻译的产品代称:

而我们重要的是获取有道的普通文本翻译逆向API,所以重要的是webfanyi-key-getter-2025会不会变。

通过查阅去年历史记录可以发现keyid是会变更的,今年只是在后面加上了”-2025“,同时拿到了一个我称之为constSign的常量密钥,我们现在还不知道他能干什么,但是先留下来。
我们既然已知所有的产品代称都是写在app.js中的,不妨写一个python脚本直接抓取js中的内容:
通过匹配webfanyi的内容,直接查找
def getProductKeys(key="webfanyi", useTemp = False):
"""获取有道翻译产品ID与产品密钥"""
if useTemp:
return "webfanyi-key-getter-2025", "yU5nT5dK3eZ1pI4j"
url = "https://shared.ydstatic.com/dict/translation-website/0.6.6/js/app.78e9cb0d.js"
data = requests.get(url).text
keyid_pattern = r'async\(\{commit:e\},t\)\=\>\{const\s+a="'+key+'([^"]+)",n="([^"]+)"'
match = re.search(keyid_pattern, data)
if match:
keyid = key+match.group(1)
const_sign = match.group(2)
return keyid, const_sign
return "webfanyi-key-getter-2025", "yU5nT5dK3eZ1pI4j"
我们已知今年的默认的keyid与产品密钥secretKey,故可以直接return。
接下来我们分析sign与mysticTime。

通过网络请求打断点,我们可以发现mysticTime只不过是一个时间戳,我们用python可以轻而易举地生成一个时间戳:
import ime
mysticTime = str(int(time.time()*1000))
同时,mysticTime时间戳还参加了sign签名的合成:

至此,我们知道了sign签名的合成方式,以及sign是一个md5串。通过断点调试同时知道了所有的返回参数:client是”fanyideskweb”, mysticTime是时间戳,product是”webfanyi”,key是产品密钥。
接下来用python计算sign即可。
import hashlib
def getSign(constSign):
"""根据产品密钥生成加密签名"""
mysticTime = str(int(time.time() * 1000))
sign = f"client=fanyideskweb&mysticTime={mysticTime}&product=webfanyi&key={constSign}"
return hashlib.md5(sign.encode('utf-8')).hexdigest()
传入API
mysticTime和sign分析完毕了,接下来传入有道给出的页面翻译api就非常容易了:
def getKeys():
"""获取有道secertKey和AES加密的密钥"""
keyid, constSign = getProductKeys()
sign = getSign(constSign)
req = requests.get(
"https://dict.youdao.com/webtranslate/key",
params = {
"keyid": keyid,
"sign": sign,
"client": "fanyideskweb",
"product": "webfanyi",
"appVersion": "1.0.0",
"vendor": "web",
"pointParam": "client,mysticTime,product",
"mysticTime": str(int(time.time() * 1000)),
"keyfrom": "fanyi.web",
"mid": "1",
"screen": "1",
"model": "1",
"network": "wifi",
"abtest": "0",
"yduuid": "abcdefg"
},
headers={
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Connection": "keep-alive",
"Content-Type": "application/x-www-form-urlencoded",
"Origin": "https://fanyi.youdao.com",
"Referer": "https://fanyi.youdao.com/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-site",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"sec-ch-ua": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\""
}
)
return req.json()['data']
请求体的headers写了一堆,防止有道识别到我们是爬虫。
这个api返回的结构是这样的:

至此,aesKey、aesKey、secretKey已经到手,下一步就是分析翻译请求。
翻译请求

翻译请求体如下,发现还有一个sign签名,通过i传入翻译内容,除去mysticTime时间戳与sign签名与i翻译内容其他照搬即可。
keyid, constSign = getProductKeys()
keys = getKeys()
aeskey, aesiv, secretKey = keys['aesKey'], keys['aesIv'], keys['secretKey']
content = "Hello world!"
sign = getSign(constSign)
mysticTime = str(int(time.time())*1000)
data = {
"i": content,
"from": "auto",
"to": "",
"useTerm": "false",
"dictResult": "true",
"keyid": "webfanyi",
"sign": sign,
"client": "fanyideskweb",
"product": "webfanyi",
"appVersion": "1.0.0",
"vendor": "web",
"pointParam": "client,mysticTime,product",
"mysticTime": mysticTime,
"keyfrom": "fanyi.web",
"mid": "1",
"screen": "1",
"model": "1",
"network": "wifi",
"abtest": "0",
"yduuid": "abcdefg"
}
req = requests.post(
"https://dict.youdao.com/webtranslate",
data=data,
headers=headers #照搬getKeys()的headers
)
当我们满怀自信的发出请求,想要看一看传过来的是什么鬼时:
b'Z21kD9ZK1ke6ugku2ccWuwRmpItPkRr5XcmzOgAKD0GcaHTZL9kyNKkN2aYY6yiOsLn2LgwT3M7YfpS83Et8_-h-T2llX1W8o8f8AAIdVCZj_25XJLSNk4MewoP55Y1cbwn8iz3yASTwzqRaCtsJ9FGdSPF-SvhmQ3wJMIWXXZqvzJZKCuu7mBoa9xmfiRgJXmJhg_MRrD-e18WoIe0fGaktOlQJPX14xqTrYaQRlOXbBF4aeEWYBf-dh9-ZqcFP7jSJGEG9zJhH7oT-VskBzA=='
这是什么鬼?而且我们发现,相同的翻译请求内容多次重复,发现返回的东西根本不是一样的!

(上面的代码是在解密工作完成后出现的错误,我提前到此处讲)
说明我们的代码被检测到了是爬虫,有道故意返回一堆错误文摘给我们。
翻译请求 – sign

我们回到断点,发现继续往下执行,函数function k居然执行了两次!我们先前仅分析了获取aesKey等的时候执行的函数,但是此时第二次执行我们没有注意到。
仔细分析即可发现,传入的参数e由产品密钥constSign变为了secretKey!

所以我们知道了,secretKey是翻译请求密钥,如果api检测到了翻译请求密钥是错误的,将会随机摘选一段文本返回给我们。
keyid, constSign = getProductKeys()
keys = getKeys()
aeskey, aesiv, secretKey = keys['aesKey'], keys['aesIv'], keys['secretKey']
content = "Hello world!"
sign = getSign(secretKey) # 请求翻译时使用secretKey,获取密钥时使用constSign
mysticTime = str(int(time.time())*1000)
data = {
"i": content,
"from": "auto",
"to": "",
"useTerm": "false",
"dictResult": "true",
"keyid": "webfanyi",
"sign": sign,
"client": "fanyideskweb",
"product": "webfanyi",
"appVersion": "1.0.0",
"vendor": "web",
"pointParam": "client,mysticTime,product",
"mysticTime": mysticTime,
"keyfrom": "fanyi.web",
"mid": "1",
"screen": "1",
"model": "1",
"network": "wifi",
"abtest": "0",
"yduuid": "abcdefg"
}
req = requests.post(
"https://dict.youdao.com/webtranslate",
data=data,
headers=headers #照搬getKeys()的headers
)
我们将获取翻译内容的函数内,请求部分的sign替换为了secretKey,结果证明我们是正确的,返回的加密内容长度变为一致的了。
AES解码翻译内容
我们该如何大战这一坨翻译内容其实已经很明显了,有道的API已经提供给我们了aesKey与aesIv,传入解码即可,根据app.js内容可以发现:

其中的t和a分别就是aesKey与aesIv。
而对aesKey和aesIv进行了function T操作,前文已经知道function T只是一个md5加密函数。
至此可以进行解码操作:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
encodeAesKey, encodeAesIv = hashlib.md5(
aeskey.encode()
).digest(), hashlib.md5(
aesiv.encode()
).digest() # 对aeskey和aesiv进行md5加密操作
cipher = AES.new(encodeAesKey, AES.MODE_CBC, encodeAesIv)
ctxs = base64.urlsafe_b64decode(req.text)
decrypted = unpad(cipher.decrypt(ctxs), AES.block_size)
print(decrypted.decode())
return decrypted.decode('utf-8')
全部代码
https://github.com/Himpq/youdao-translate-api – 有道翻译逆向api
注意事项
该接口存在诸多限制,字词数量限制未知,但是短期大量访问会产生错误。