JS逆向实战:解密有道翻译前端加密与签名机制

1. 项目概述:从一次翻译请求说起

最近在整理一些外文资料,需要批量处理,自然就想到了调用翻译接口。市面上公开的API要么有额度限制,要么就是收费的。于是,我像很多开发者一样,把目光投向了那些提供免费网页翻译服务的大厂,比如有道翻译。直接打开网页,输入文本,点击翻译,一切看起来都很顺畅。但当我们想用程序自动化这个流程时,问题就来了:直接模拟一个POST请求,把文本发过去,返回的往往是错误页面或者风控拦截。

这就是典型的 前端加密 场景。为了反爬虫和保障服务安全,网站会在浏览器端(即JavaScript执行环境)对请求参数进行加密或签名,使得简单的请求复制失效。要突破这层障碍,就必须深入浏览器内部,搞清楚JavaScript代码到底对我们的输入做了什么手脚。这个过程,就是我们常说的 JS逆向 。今天,我就以“某道翻译”这个非常经典且适合练手的案例,带你完整走一遍JS逆向的分析、定位、还原与复现流程。无论你是刚接触爬虫的新手,还是想巩固逆向思路的老兵,这个案例都能让你收获颇丰。

2. 逆向目标分析与环境准备

在动手之前,我们必须明确目标。我们的核心目标是: 模拟有道翻译网页版(fanyi.youdao.com)的翻译请求,成功获取翻译结果

2.1 核心需求拆解

要实现这个目标,我们需要解决几个具体问题:

  1. 找到真正的翻译接口 :翻译动作触发的网络请求地址是什么?
  2. 识别关键加密参数 :这个接口需要哪些参数?其中哪些是动态生成、被加密的?
  3. 定位加密函数 :在庞大的JS代码库中,找到生成这些加密参数的JavaScript函数。
  4. 理解加密逻辑 :分析该函数的输入、输出和内部算法。
  5. 本地复现加密 :使用Python(或其他语言)重写加密逻辑,生成合法的参数。
  6. 组装请求并获取数据 :使用复现的加密算法生成参数,模拟浏览器发送请求,解析返回的翻译结果。

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 捕获翻译请求

  1. 打开开发者工具(F12),切换到 Network(网络) 面板。
  2. 确保录制按钮是红色开启状态,并勾选上 “Preserve log”(保留日志) ,防止页面跳转时请求记录被清空。
  3. 在翻译页面的输入框中,输入一个测试文本,例如 “hello world”,然后点击“翻译”按钮。
  4. 此时,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 搜索与断点调试

  1. 全局搜索 :在开发者工具的 Sources 面板,按 Ctrl+Shift+F (Windows) 或 Cmd+Opt+F (Mac) 打开全局搜索。尝试搜索这些参数名,如 sign: salt: bv: 。更有效的方法是搜索这些参数名的一部分,或者搜索可能赋值的关键字,如 sign = MD5 (因为 sign 看起来像MD5哈希值)。
  2. XHR断点 :在 Sources 面板,找到 “XHR/fetch Breakpoints” 区域,点击 “+” 号,添加一个包含部分接口URL的断点,例如 translate_o 。然后重新触发翻译动作。浏览器会在发起这个XHR请求前暂停,此时调用栈(Call Stack)会显示导致这个请求的所有JavaScript函数,我们可以顺着调用栈一步步往回找,找到设置参数的地方。
  3. 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 里进行验证。

  1. 将疑似函数代码复制到Console中(可能需要补全它依赖的 n.md5 函数,通常 n 是一个包含MD5方法的工具对象,可以从源代码里找到并一并复制)。
  2. 定义一个变量 e "hello world" ,然后调用这个函数 r(e)
  3. 观察输出对象,对比其 sign salt bv 的值,是否与刚才Network抓包中看到的参数值 形态一致 (例如, sign 是否是32位十六进制字符串)。
  4. 重新触发一次翻译,抓取新的参数,然后用同样的输入 "hello world" 在Console里再执行一次函数。你会发现,因为 salt 中的随机数和 ts 的变化,两次生成的 sign 完全不同,但 算法模式 应该是一样的。

通过Console验证,是确保我们找对函数的 关键一步 ,能避免后续很多徒劳的努力。

5. 加密算法本地复现(Python实现)

确认了加密函数,接下来就是要在我们的Python脚本中复现这个逻辑。有两种主流策略:

5.1 策略一:纯Python重写(推荐,便于理解)

如果加密逻辑不复杂(如本例的MD5),强烈建议用Python重写。这有助于彻底理解算法,且执行效率高,依赖少。

首先,我们需要从JS代码中提取出关键信息:

  1. 固定密钥(secretKey) :本例中是 "Ygy_4c=r#e#4EX^NUGUc5"
  2. bv 的生成方式 MD5(navigator.appVersion) navigator.appVersion 是浏览器用户代理的一部分,我们可以用一个固定的、合理的字符串来模拟,例如 "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" 。在实际爬虫中,为了更仿真,可以使用 fake_useragent 库生成一个,然后取它的MD5。但经过测试,有道翻译对这个值的校验并不严格,一个固定的、符合格式的MD5值通常也能工作。
  3. 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 来调用它。

步骤:

  1. 创建一个 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;
    }
    
  2. 在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 。这通常意味着触发了反爬机制。可以尝试以下策略:

  1. 添加Cookies :从浏览器复制一组有效的Cookie到头信息中。
  2. 使用Session :使用 requests.Session() 对象,它会自动管理Cookie,模拟浏览器会话。
  3. 降低请求频率 :在请求间增加随机延时 time.sleep(random.uniform(1, 3))
  4. 更换User-Agent :使用 fake_useragent 库随机生成。
  5. 使用代理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逆向中最常见的形式之一。掌握这个案例,你就拿到了打开许多类似网站前端加密大门的第一把钥匙。逆向工程没有一成不变的套路,核心是耐心观察、大胆假设、小心验证。每一次成功的逆向,都是对开发者思维的一次绝佳训练。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值