有道翻译JS 逆向分析与实现
评论
收藏

有道翻译JS 逆向分析与实现

经验分享
月色ono
2025-11-13 13:46·浏览量:776
月色ono
影刀高级开发者
发布于 2025-11-13 13:40更新于 2025-11-13 13:46776浏览

📖 项目概述

分析有道翻译 Web 版(https://fanyi.youdao.com)的 API 接口,通过逆向工程破解其加密机制,实现完整的翻译功能 Python 实现。

🔍 逆向分析过程

1. 网络请求分析

这种响应看不懂的肯定是加密了

通过浏览器开发者工具抓包分析,发现主要涉及两个关键接口:

  • 密钥获取接口:GET https://dict.youdao.com/webtranslate/key
  • 翻译接口:POST https://dict.youdao.com/webtranslate

tips:sign参数和响应结果需要解密

小技巧快速生成py代码

右键对应的包,复制,curl格式复制

爬虫工具网:https://www.spidertools.cn/#/curl2Request

2. 请求参数分析

翻译请求的主要参数结构:

data = {
    "i": "苹果",           # 需要翻译的文本
    "from": "auto",        # 源语言(自动检测)
    "to": "",              # 目标语言
    "useTerm": "false",
    "domain": "0",
    "dictResult": "true",  # 是否返回字典结果
    "keyid": "webfanyi",   # 固定值
    "sign": "d50df236da2f8fb02aa35ced782a9e0e",  # 动态签名
    "client": "fanyideskweb",     # 固定值
    "product": "webfanyi",        # 固定值
    "appVersion": "1.0.0",        # 固定值
    "vendor": "web",              # 固定值
    "pointParam": "client,mysticTime,product",  # 固定值
    "mysticTime": "1763003045609",  # 时间戳
    "keyfrom": "fanyi.web",       # 固定值
    "mid": "1",                   # 固定值
    "screen": "1",                # 固定值
    "model": "1",                 # 固定值
    "network": "wifi",            # 固定值
    "abtest": "0",                # 固定值
    "yduuid": "abcdefg"           # 固定值
}

3. 关键加密参数破解

快速定位:

打开搜索框,搜索网址后面的定位这个包发起http协议的位置,

没混淆,再对应js文件搜索如sign一般不是sign=,就是sign:有的.sign有的要打引号之类的大致就那些类型,这个就看经验了

正常是打断点定位,这种直接有的就不用,指导找到,然后看下对应的值

3.1 签名生成算法

通过 JavaScript 逆向分析,发现签名生成逻辑:

python

def _generate_sign(self, mystic_time: int, key: str) -> str:
    """生成请求签名"""
    raw = f"client={CLIENT}&mysticTime={mystic_time}&product={PRODUCT}&key={key}"
    return hashlib.md5(raw.encode('utf-8')).hexdigest()

3.2 动态密钥获取

打上点调试,鼠标放在上面就显示对应的值

mystictime是时间戳的d,u都是固定的 就这个t要看下,这个t咋一看有点熟悉,发现前面抓包的时候在这个请求前还有一个包

secretKey这个跟这个t不说10分相似起码也有个9分相似,加上返回了aesiv和aeskey我猜测那个响应加密是不是就是aes加密.接下来就是继续跟或者找个在线解密的测试一下,我继续调试,很快就找到了密文的位置

跟进去看看确实是AES加密算法

在翻译请求之前,需要先获取动态加密密钥:

python

def fetch_aes_keys(self) -> dict:
    """从服务器获取动态加密密钥"""
    url = "https://dict.youdao.com/webtranslate/key"
    mystic_time = str(int(time.time() * 1000))
    sign = self._generate_sign(mystic_time, DEFAULT_KEY)
    
    # 构建请求参数...
    response = self.session.get(url, params=params)
    data = response.json().get("data", {})
    return {
        "secret_key": data["secretKey"],  # 用于签名
        "aes_key": data["aesKey"],        # 用于响应解密
        "aes_iv": data["aesIv"]           # 用于响应解密
    }

4. 响应解密机制

服务器返回的响应数据使用 AES-CBC 加密,解密流程:

python

def decrypt_response(self, ciphertext_b64: str, aes_key: str, aes_iv: str) -> str:
    """解密 AES-CBC 加密的响应数据"""
    # Base64 解码
    cipher_bytes = urlsafe_b64decode(ciphertext_b64)
    
    # 从字符串生成 AES 密钥和 IV
    key = hashlib.md5(aes_key.encode('utf-8')).digest()
    iv = hashlib.md5(aes_iv.encode('utf-8')).digest()
    
    # AES 解密
    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypted_padded = cipher.decrypt(cipher_bytes)
    
    # PKCS7 去填充
    decrypted = unpad(decrypted_padded, block_size=16, style='pkcs7')
    return decrypted.decode('utf-8')

🛠 完整 Python 实现

核心配置

"""
Youdao Web Translate API 封装模块
支持:自动获取密钥、AES解密、结果解析、格式化输出
"""

import time
import hashlib
import requests
import json
from base64 import urlsafe_b64decode
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

# ==================== 配置 ====================
HEADERS = {
    "Accept": "application/json, text/plain, */*",
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "Content-Type": "application/x-www-form-urlencoded",
    "Origin": "https://fanyi.youdao.com",
    "Pragma": "no-cache",
    "Referer": "https://fanyi.youdao.com/",
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-site",
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
    "sec-ch-ua": '"Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": '"macOS"'
}

COOKIES = {
    "OUTFOX_SEARCH_USER_ID": "-959393723@218.1.219.199",
    "OUTFOX_SEARCH_USER_ID_NCOO": "106810021.57668559",
    "DICT_DOCTRANS_SESSION_ID": "MGFmMjNlMGMtYzhhMS00MzlmLWE3ODktNTc3MGRiNTgwNWJj",
    "_uetsid": "ff8d75d0b3a711f0bcc755bc4bf023ab",
    "_uetvid": "7be93100979311f0bc83d941597f7661"
}

CLIENT = "fanyideskweb"
PRODUCT = "webfanyi"
DEFAULT_KEY = "yU5nT5dK3eZ1pI4j"  # 初始key,用于获取 secretKey


# ==================== 工具函数 ====================
def _md5_hex(text: str) -> str:
    """计算字符串的 MD5 哈希值(十六进制小写)"""
    return hashlib.md5(text.encode('utf-8')).hexdigest()

def _md5_bytes(data: str) -> bytes:
    """返回 MD5 哈希的原始字节"""
    return hashlib.md5(data.encode('utf-8')).digest()


# ==================== 核心接口类 ====================
class YouDaoTranslator:
    def __init__(self):
        self.session = requests.Session()
        self.session.headers.update(HEADERS)
        self.session.cookies.update(COOKIES)

    def _generate_sign(self, mystic_time: int, key: str) -> str:
        """生成请求签名"""
        raw = f"client={CLIENT}&mysticTime={mystic_time}&product={PRODUCT}&key={key}"
        return _md5_hex(raw)

    def _generate_request_headers(self, sign: str, mystic_time: int) -> dict:
        """构建包含认证信息的请求头"""
        return {
            "client": CLIENT,
            "product": PRODUCT,
            "appVersion": "1.0.0",
            "vendor": "web",
            "pointParam": "client,mysticTime,product",
            "mysticTime": str(mystic_time),
            "keyfrom": "fanyi.web",
            "mid": "1",
            "screen": "1",
            "model": "1",
            "network": "wifi",
            "abtest": "0",
            "yduuid": "abcdefg",
            "sign": sign
        }

    def fetch_aes_keys(self) -> dict:
        """从服务器获取动态加密密钥(secretKey, aesKey, aesIv)"""
        url = "https://dict.youdao.com/webtranslate/key"
        mystic_time = str(int(time.time() * 1000))
        sign = self._generate_sign(mystic_time, DEFAULT_KEY)

        params = {
            "keyid": "webfanyi-key-getter-2025",
            "sign": sign,
            "client": CLIENT,
            "product": PRODUCT,
            "appVersion": "1.0.0",
            "vendor": "web",
            "pointParam": "client,mysticTime,product",
            "mysticTime": mystic_time,
            "keyfrom": "fanyi.web",
            "mid": "1",
            "screen": "1",
            "model": "1",
            "network": "wifi",
            "abtest": "0",
            "yduuid": "abcdefg"
        }

        try:
            resp = self.session.get(url, params=params)
            resp.raise_for_status()
            data = resp.json().get("data", {})
            return {
                "secret_key": data["secretKey"],
                "aes_key": data["aesKey"],
                "aes_iv": data["aesIv"]
            }
        except Exception as e:
            raise RuntimeError(f"获取密钥失败: {e}")

    def translate(self, word: str) -> str:
        """发送翻译请求(返回可能是密文或明文)"""
        keys = self.fetch_aes_keys()
        mystic_time = int(time.time() * 1000)
        sign = self._generate_sign(mystic_time, keys["secret_key"])

        full_headers = {**HEADERS, **self._generate_request_headers(sign, mystic_time)}

        data = {
            "i": word,
            "from": "auto",
            "to": "",
            "useTerm": "false",
            "dictResult": "true",
            "keyid": "webfanyi",
            "sign": sign,
            "client": CLIENT,
            "product": PRODUCT,
            "appVersion": "1.0.0",
            "vendor": "web",
            "pointParam": "client,mysticTime,product",
            "mysticTime": mystic_time,
            "keyfrom": "fanyi.web",
            "mid": "1",
            "screen": "1",
            "model": "1",
            "network": "wifi",
            "abtest": "0",
            "yduuid": "abcdefg"
        }

        try:
            resp = self.session.post("https://dict.youdao.com/webtranslate", headers=full_headers, data=data)
            resp.raise_for_status()
            return resp.text.strip()
        except Exception as e:
            raise RuntimeError(f"翻译请求失败: {e}")

    def decrypt_response(self, ciphertext_b64: str, aes_key: str, aes_iv: str) -> str:
        """解密 AES-CBC 加密的响应数据"""
        try:
            # 补齐 Base64 padding
            missing = len(ciphertext_b64) % 4
            if missing:
                ciphertext_b64 += '=' * (4 - missing)

            cipher_bytes = urlsafe_b64decode(ciphertext_b64)
            key = _md5_bytes(aes_key)
            iv = _md5_bytes(aes_iv)

            cipher = AES.new(key, AES.MODE_CBC, iv)
            decrypted_padded = cipher.decrypt(cipher_bytes)
            decrypted = unpad(decrypted_padded, block_size=16, style='pkcs7')
            return decrypted.decode('utf-8')
        except Exception as e:
            raise RuntimeError(f"解密失败: {e}")


# ==================== 结果解析器 ====================
class YouDaoParser:
    @staticmethod
    def parse(result: str) -> dict:
        """将 JSON 字符串解析为 Python 字典"""
        if isinstance(result, dict):
            return result
        return json.loads(result)

    @staticmethod
    def format_meaning(data: dict) -> None:
        """格式化并打印单词详细信息"""
        ec = data.get("dictResult", {}).get("ec", {}) or {}
        word_info = ec.get("word", {})
        trs = word_info.get("trs", [])
        wfs = word_info.get("wfs", [])
        exam_types = ec.get("exam_type", [])

        src = data.get("translateResult", [[{"src": "", "tgt": ""}]])[0][0]["src"]
        tgt = data.get("translateResult", [[{"src": "", "tgt": ""}]])[0][0]["tgt"]

        print(f"📘 单词: {src} → {tgt}")
        print()

        usphone = word_info.get("usphone", "")
        ukphone = word_info.get("ukphone", "")
        if usphone or ukphone:
            print(f"🇬🇧 英音: /{ukphone}/")
            print(f"🇺🇸 美音: /{usphone}/")
            print()

        for item in trs:
            print(f"{item.get('pos', '')} {item.get('tran', '')}")
        print()

        if exam_types:
            print("🔖 考试标签:", " / ".join(exam_types))
            print()

        if wfs:
            print("🔄 词形变化:")
            for wf_item in wfs:
                name = wf_item["wf"]["name"]
                value = wf_item["wf"]["value"]
                print(f"  • {name}: {value}")
            print()


# ==================== 简化接口 ====================
def lookup(word: str) -> None:
    """
    快速查词接口
    示例: lookup("append")
    """
    translator = YouDaoTranslator()
    try:
        raw_response = translator.translate(word)

        # 尝试判断是否为加密数据(Base64)
        if raw_response.startswith('{') and 'code' in raw_response:
            # 是明文 JSON
            parsed = json.loads(raw_response)
        else:
            keys = translator.fetch_aes_keys()
            decrypted = translator.decrypt_response(raw_response, keys["aes_key"], keys["aes_iv"])
            parsed = json.loads(decrypted)

        YouDaoParser.format_meaning(parsed)

    except Exception as e:
        print(f"❌ 查询失败: {e}")


# ==================== 使用示例 ====================
if __name__ == "__main__":
    lookup("眼睛")

收藏12
全部评论1
最新
发布评论
评论