有道翻译 逆向API 分析

声明:本文章仅用于学习探讨,请勿用于任何违反法律的用途,如有侵权请联系下架。

有道web翻译请求流程

看着是不是很简单,但是逆向分析你的头会变大。

第一次请求分析

通过控制台监听网络可以发现,最重要的请求参数就是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返回的结构是这样的:

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

翻译请求

翻译请求体如下,发现还有一个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已经提供给我们了aesKeyaesIv,传入解码即可,根据app.js内容可以发现:

其中的t和a分别就是aesKeyaesIv

而对aesKeyaesIv进行了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

注意事项

该接口存在诸多限制,字词数量限制未知,但是短期大量访问会产生错误。

发表回复