Token加密逆向实战:从JS调试到Python还原的完整爬虫解决方案

1. 项目概述:一次针对Token加密机制的深度逆向之旅

最近在做一个数据采集项目时,遇到了一个相当“硬核”的坎儿:目标平台的核心API请求,其身份验证Token不再是简单的明文或Base64编码,而是被一套复杂的客户端加密算法保护了起来。直接抓包拿到的请求参数,里面的Token字段是一串毫无规律、每次请求都在变化的密文。这意味着,传统的爬虫手段——复制Cookie、复制Authorization头——在这里完全失效了。这激起了我的技术好奇心,也迫使我必须深入客户端内部,去搞清楚这个Token到底是怎么生成的。今天,我就把这次完整的“逆向分析+Python还原”实战过程记录下来,这不仅仅是一个爬虫案例,更是一次对现代Web应用安全机制的理解与挑战。

这个项目适合所有对网络爬虫进阶、Web安全、JavaScript逆向以及Python自动化有兴趣的朋友。无论你是遇到了类似的技术壁垒,还是单纯想了解前端加密与后端验证是如何“斗智斗勇”的,这篇文章都将提供一个非常详实的参考路径。整个过程涉及静态分析、动态调试、算法还原和代码复现,我会尽量用通俗的语言把每个环节讲清楚,并提供可以直接“抄作业”的代码片段。当然,我们必须时刻牢记 合规底线 :所有的分析仅用于学习交流与技术研究,务必尊重网站的 robots.txt 协议,控制请求频率,避免对目标服务器造成压力,更不得用于非法获取和滥用数据。毕竟,把正规爬虫的带宽都挤占了的做法,是损人不利己的。

2. 逆向分析的核心思路与工具准备

面对一个加密的Token,我们的核心目标很明确:找到生成这个Token的原始逻辑,并用Python将其复现出来。这听起来像在破解一个黑盒,而我们的武器就是逆向工程。整个思路可以概括为“由外及内,动静结合”。

2.1 逆向分析的基本逻辑链条

首先,我们需要建立一个清晰的逻辑链条。一个典型的客户端加密Token的生成流程,通常离不开以下几个关键点:

  1. 入口定位 :Token在何处被生成并设置到请求中?通常是在发起网络请求(如 XMLHttpRequest fetch )之前,由某个JavaScript函数处理请求参数时加入的。
  2. 关键函数定位 :找到负责生成或加密Token的核心函数。这个函数可能被混淆、被压缩,但它的输入(通常是用户信息、时间戳、随机数等)和输出(加密后的Token)是确定的。
  3. 算法识别 :分析该函数内部使用的加密算法。是常见的AES、RSA,还是自定义的哈希、编码组合?参数(如密钥、加密模式、填充方式)是什么?
  4. 依赖追踪 :加密函数可能依赖其他函数或全局变量(例如,一个动态生成的密钥)。需要理清这些依赖关系。
  5. 环境还原 :有些加密逻辑依赖于浏览器环境特有的对象或属性(如 window document 的一些特性),在Node.js或Python中需要模拟。

2.2 必备工具栈

工欲善其事,必先利其器。以下是本次逆向分析中我用到的主要工具,它们构成了从探查到复现的完整流水线。

  • 浏览器开发者工具(Chrome DevTools) :这是我们的主战场。尤其是 Sources 面板和 Network 面板。
    • Network面板 :用于捕获所有网络请求,重点关注携带加密Token的请求。查看其请求头(Headers)和发起者(Initiator),可以快速定位到是哪个脚本文件发起了这个请求,从而缩小代码搜索范围。
    • Sources面板 :用于静态查看和动态调试JavaScript代码。可以在这里搜索关键词(如Token的字段名 token sign encrypt 等),设置断点(Breakpoint),单步执行(Step into/over)来观察变量状态。
  • Overrides功能 :Chrome DevTools的一个神器。它允许你将在线JS文件映射到本地副本。这样你就可以在本地随意修改、格式化(美化)混淆的代码,刷新页面后浏览器会加载你修改过的本地文件,极大方便了动态调试和代码分析。
  • Python环境 :用于最终算法还原和爬虫编写。需要准备一些关键库:
    • requests :用于发送HTTP请求。
    • execjs :一个执行JavaScript代码的库。在完全用Python还原算法困难时,可以“偷懒”直接调用关键的、已提取的JS函数。
    • PyExecJS execjs 的一个后端,需要安装一个JavaScript运行环境,如 Node.js
    • Crypto 相关库:如 pycryptodome ,用于实现AES、RSA等标准加密算法。
  • 代码格式化工具 :如果目标JS代码被压缩成一行,你需要一个格式化工具。在线工具(如 beautifier.io )或编辑器的插件(如Prettier)都可以。格式化后的代码可读性会大大提升。

注意 :在开始逆向之前,一定要在Network面板中勾选“Preserve log”(保留日志),并禁用缓存(Disable cache),以确保能捕获到页面加载过程中的所有请求,尤其是第一次生成Token的请求。

2.3 初步探查与关键词搜索

打开目标网站,进行登录或触发需要Token的API操作。在Network面板中,找到那个返回数据或提交数据的XHR/Fetch请求。查看其请求头或请求体,找到那个加密的Token字段,记住它的名字,比如叫 encryptedToken signature

然后,在Sources面板中,使用 Ctrl+Shift+F 进行全局搜索。搜索关键词可以包括:

  • Token字段名本身: encryptedToken
  • 可能的方法名: encrypt sign getToken generateSign
  • 常见的加密库标识: CryptoJS encrypt AES RSA
  • 设置请求头的代码: setRequestHeader

搜索的结果可能会指向一个被压缩的JS文件。这时,首先点击左下角的 {} (Pretty-print)按钮格式化代码,然后在该文件中继续搜索,逐步缩小核心函数的位置。

3. 动态调试与关键函数定位实战

静态搜索往往只能找到线索,真正的突破来自于动态调试。我们需要让代码在生成Token的那一刻暂停下来,让我们有机会观察所有的输入、输出和中间状态。

3.1 断点策略与调用栈分析

找到疑似生成Token的代码行后,在其左侧行号处点击设置断点。更高效的方法是使用 “事件监听器断点” (Event Listener Breakpoints)。在Sources面板的右侧,展开“Event Listener Breakpoints”,勾选“XHR/Fetch”下的 send fetch 。这样,当任何JavaScript代码发起网络请求时,执行都会自动暂停,我们可以直接看到是哪个函数在发起请求,并顺着调用栈(Call Stack)向上回溯,找到封装和加密参数的逻辑。

当断点触发后,右侧的“Call Stack”区域会显示当前执行位置的函数调用链。点击调用栈中的上层函数,可以跳转到对应的代码位置,观察参数是如何一层层传递和加工的。我们的目标,就是找到那个最终将原始数据(如 {“userId”: 123, “timestamp”: …} )转换成加密字符串的函数。

3.2 关键变量监控与作用域分析

在断点暂停的状态下,调试器的“Scope”区域显示了当前作用域内的所有变量。你可以将鼠标悬停在代码中的变量上,或者直接在Console面板中输入变量名来查看其当前值。这是理解算法输入的关键。

例如,你可能会发现一个名为 rawData 的对象,里面包含了时间戳、用户ID和一些固定参数。然后,看到这个 rawData 被传递给一个名为 encryptData 的函数,该函数返回的结果最终被赋值给了请求头中的 X-Token 字段。那么, encryptData 就是我们需要重点攻克的核心函数。

3.3 跟踪函数执行与参数提取

进入 encryptData 函数内部,可以继续使用单步调试(F11 Step into)。观察每一步执行后,数据发生了怎样的变化。是进行了字符串拼接?还是调用了某个加密库的方法?特别注意函数调用时传入的参数,以及函数返回的结果。

一个非常实用的技巧是:在Console中,你可以尝试 手动执行你怀疑的核心函数 。首先,在代码中找到这个函数的定义,通常是一个 function encryptData(data) {…} 或者 var encrypt = function(){…} 。然后,在Console中,复制这个函数的完整代码(包括其依赖的任何全局变量或工具函数),定义一个同名函数。接着,构造一个你认为可能的输入参数,手动调用这个函数,看输出是否与网络请求中捕获的Token一致或类似。如果一致,恭喜你,你已经成功提取了加密逻辑。

实操心得 :很多加密函数会依赖一个“密钥”(key)。这个密钥可能是硬编码在JS文件里的一个字符串,也可能是通过另一个函数动态生成的,甚至是从服务器端响应中获取的。在调试时,一定要找到这个密钥的源头。如果密钥是动态生成的,你需要把生成密钥的逻辑也一并提取出来。

4. 算法还原与Python代码实现

经过动态调试,我们假设已经定位到了核心加密函数 generateToken(payload) 。通过观察和手动执行,我们分析出它的逻辑是:

  1. 将载荷 payload (一个对象)按固定键顺序排序后,转换为 key1=value1&key2=value2 格式的字符串。
  2. 在这个字符串末尾拼接一个固定的密钥 secret
  3. 对拼接后的字符串进行MD5哈希(也可能是SHA1、SHA256等)。
  4. 将MD5结果进行Base64编码,得到最终的Token。

4.1 纯Python还原实现

对于这种标准算法(MD5、Base64),我们可以用Python的 hashlib base64 库轻松还原。

import hashlib
import base64
import urllib.parse
import time
import json

def generate_token_pure(payload_dict, secret='your_secret_key_here'):
    """
    纯Python实现Token生成算法
    Args:
        payload_dict: 字典,包含需要加密的数据,如userId, timestamp等。
        secret: 字符串,从JS中分析得到的固定密钥。
    Returns:
        str: 加密后的Token字符串。
    """
    # 1. 对字典按键进行排序,并转换为查询字符串格式
    # 注意:排序是为了保证顺序与JS端一致,这是很多签名算法的常见要求
    sorted_items = sorted(payload_dict.items(), key=lambda x: x[0])
    query_string = '&'.join([f'{k}={v}' for k, v in sorted_items])

    # 2. 拼接密钥
    sign_string = query_string + secret

    # 3. 计算MD5哈希(注意编码)
    # JS中通常处理的是UTF-8或特定编码的字符串,Python需保持一致
    md5_hash = hashlib.md5(sign_string.encode('utf-8')).hexdigest()

    # 4. 进行Base64编码
    # 注意:有些JS的Base64实现可能对结果有处理(如替换字符),需观察确认
    token = base64.b64encode(md5_hash.encode('utf-8')).decode('utf-8')
    # 示例中可能还会去掉末尾的‘=’
    token = token.rstrip('=')

    return token

# 示例用法
payload = {
    'userId': '123456',
    'timestamp': int(time.time() * 1000), # JS常用毫秒时间戳
    'nonce': 'abcdefg'
}
secret = 'this_is_a_hardcoded_secret' # 此密钥需从JS中分析得出

my_token = generate_token_pure(payload, secret)
print(f"生成的Token: {my_token}")

4.2 使用execjs调用提取的JS函数

有些加密算法非常复杂,或者大量使用了浏览器环境特有的对象,完全用Python重写成本很高。这时, execjs 就派上用场了。我们可以把从JS文件中提取出来的、包含核心加密逻辑的代码片段保存到一个 .js 文件中,然后用Python去调用它。

假设我们提取出的关键JS代码保存为 crypto_logic.js

// crypto_logic.js
// 这里是从目标网站JS中提取并可能稍作修改的加密函数
function complexEncrypt(data, dynamicKey) {
    // ... 这里是非常复杂的、依赖浏览器Crypto API或特定库的加密逻辑 ...
    // 例如,使用了 window.crypto.subtle 或某个混淆过的第三方库
    var encrypted = someObfuscatedFunction(data, dynamicKey);
    return encrypted;
}

// 暴露给外部调用的接口
function getEncryptedToken(payloadJsonStr, extraParams) {
    var payload = JSON.parse(payloadJsonStr);
    var key = generateDynamicKey(extraParams); // 可能还有动态密钥生成逻辑
    return complexEncrypt(payload, key);
}

对应的Python调用代码:

import execjs
import json

# 1. 读取包含加密逻辑的JS文件
with open('crypto_logic.js', 'r', encoding='utf-8') as f:
    js_code = f.read()

# 2. 创建JS执行上下文
ctx = execjs.compile(js_code)

# 3. 准备参数
payload = {'userId': 123, 'action': 'query'}
extra = {'version': '1.0'}

# 4. 调用JS函数
# 注意:JS函数接收字符串,所以需要将字典序列化
token = ctx.call('getEncryptedToken', json.dumps(payload), extra)
print(f"通过execjs生成的Token: {token}")

这种方法的好处是还原速度快,几乎可以100%模拟原逻辑。缺点是部署环境需要安装Node.js,并且执行效率比纯Python慢。

4.3 处理动态密钥与环境依赖

这是逆向中最棘手的部分之一。密钥可能不是硬编码的,而是:

  • 由服务器下发的 :在某个初始化接口的响应中返回。这时你的爬虫需要先模拟这个初始化请求,拿到密钥。
  • 根据时间或用户信息计算的 :例如, key = md5(用户ID + 当日日期) 。你需要用Python复现这个计算过程。
  • 依赖浏览器指纹 :使用了 navigator.userAgent 、屏幕分辨率等生成一个种子。在Python中,你需要用 requests 库的 headers 来模拟一个固定的UA,并保证每次计算时使用的“指纹”一致。

对于环境依赖,比如使用了 window.btoa (Base64编码)或 CryptoJS 库。对于 btoa ,可以用Python的 base64.b64encode 替代。如果使用了 CryptoJS ,你可以尝试在Python中用 pycryptodome 实现相同算法,或者更简单地将 CryptoJS 的源码片段一起打包到你的 execjs 执行环境中。

5. 完整爬虫集成与请求模拟

成功还原Token生成算法后,剩下的工作就是将其集成到一个健壮的爬虫流程中。

5.1 爬虫流程设计

一个典型的、需要处理加密Token的爬虫流程如下:

  1. 会话维持 :使用 requests.Session() 来维持Cookie,模拟浏览器会话状态。
  2. 前置请求 :访问登录页或主页,获取必要的初始Cookie或CSRF Token。
  3. 登录模拟 :如果需要登录,模拟登录请求。登录成功后,Session会自动保存登录态Cookie。
  4. 获取动态参数 :如果Token生成依赖服务器下发的动态参数(如一个临时的 keyId ),先请求相关接口获取这些参数。
  5. 构造载荷与生成Token :根据API要求,构造 payload 字典(包含业务参数、时间戳、随机数等),调用我们还原的 generate_token 函数生成加密Token。
  6. 发起目标请求 :将生成的Token放入请求头(通常是 Authorization: Bearer <token> 或自定义头如 X-Sign )或请求体中,发起最终的API请求。
  7. 处理响应与错误 :解析响应数据。特别注意处理Token过期(返回401/403状态码)的情况,设计重试或重新登录的逻辑。

5.2 请求头与参数模拟

为了让请求更像来自真实浏览器,除了Token,还需要精心设置请求头。

import requests
import time
import random
import string

def get_random_string(length):
    """生成随机字符串,用于模拟nonce等参数"""
    letters = string.ascii_letters + string.digits
    return ''.join(random.choice(letters) for i in range(length))

session = requests.Session()

# 设置通用请求头,模拟Chrome浏览器
headers = {
    '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',
    'Accept': 'application/json, text/plain, */*',
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
    'Accept-Encoding': 'gzip, deflate, br',
    'Content-Type': 'application/json;charset=UTF-8', # 根据实际情况调整
    'Origin': 'https://target-website.com',
    'Referer': 'https://target-website.com/some/page',
}
session.headers.update(headers)

# 1. 可能的前置请求,获取初始Cookie
session.get('https://target-website.com/login')

# 2. 模拟登录(如果需要)
login_data = {
    'username': 'your_username',
    'password': 'your_password',
    # 可能还有从登录页HTML中解析出的CSRF token
}
login_resp = session.post('https://target-website.com/api/login', json=login_data)
if login_resp.status_code != 200:
    print('登录失败')
    exit()

# 3. 获取动态密钥(如果需要)
key_resp = session.get('https://target-website.com/api/getKey')
dynamic_secret = key_resp.json().get('secretKey')

# 4. 构造业务请求载荷并生成Token
payload = {
    'page': 1,
    'size': 20,
    'timestamp': int(time.time() * 1000), # 毫秒时间戳
    'nonce': get_random_string(8), # 随机数,防重放
    'userId': login_resp.json().get('userId')
}
# 假设我们使用纯Python还原的函数
from token_generator import generate_token_pure
encrypted_token = generate_token_pure(payload, dynamic_secret) # 使用动态密钥

# 5. 发起最终请求,携带加密Token
api_headers = {
    'X-Token': encrypted_token,
    # 其他业务头...
}
target_url = 'https://target-website.com/api/targetData'
resp = session.post(target_url, json=payload, headers=api_headers)

if resp.status_code == 200:
    data = resp.json()
    print('数据获取成功!')
    # 处理数据...
else:
    print(f'请求失败,状态码:{resp.status_code}')
    print(resp.text)

5.3 应对反爬策略

目标平台可能不止Token加密这一道防线。常见的还有:

  • 频率限制 :在代码中主动添加延迟, time.sleep(random.uniform(1, 3)) ,避免请求过快。
  • IP封禁 :考虑使用代理IP池。 requests 库可以通过 proxies 参数设置代理。
  • 请求头校验 :确保 User-Agent Referer Accept 等头信息与浏览器行为一致。
  • JavaScript挑战 :有些网站会先返回一段JS代码,要求客户端执行并返回结果才能继续。这通常需要 execjs Selenium 等工具来配合解决,复杂度更高。

6. 常见问题排查与调试技巧实录

在实际操作中,你几乎一定会遇到各种问题。下面是我踩过的一些坑以及排查思路。

6.1 Token生成结果不一致

这是最常见的问题。你的Python代码生成的Token和浏览器生成的Token对不上。

  • 排查步骤1:检查输入一致性 。这是最关键的。将浏览器中断点处的 payload 对象和 secret 值完整地复制出来,确保你的Python函数输入的每一个键值对、每一个字符都完全一致。特别注意:
    • 数据类型 :JS中数字 123 和字符串 ”123″ 的MD5结果不同。
    • 编码 :中文字符串的编码。JS内部通常使用UTF-16,但在进行网络传输或哈希计算时,可能会转换为UTF-8。在Python中统一使用 .encode(‘utf-8’)
    • 空格与不可见字符 :字符串首尾是否有空格?换行符是 \n 还是 \r\n
  • 排查步骤2:验证中间结果 。在JS调试器中,在加密函数的每一步,都把中间变量的值打印到Console。在你的Python代码中,也对应地打印出每一步的结果。对比从原始字符串拼接,到第一次哈希/加密前的数据,再到最终编码前的数据,看哪一步开始出现差异。
  • 排查步骤3:算法细节
    • 哈希算法 :你确定是MD5吗?会不会是SHA-1、SHA-256?哈希结果是否截取了部分字符(如前16位)?
    • Base64变种 :标准的Base64编码结果包含 +/ 和末尾的 = 。有些JS实现会进行URL安全的替换( + -> - / -> _ )并去掉填充 = 。Python的 base64.urlsafe_b64encode 可以处理前者,但需要手动去掉 =
    • 自定义编码 :可能根本不是标准Base64,而是自定义的字符映射表。

6.2 动态密钥获取失败

  • 问题 :模拟获取密钥的请求返回错误或空数据。
  • 排查
    1. 检查该请求是否需要特定的Cookie或Header(如 X-Requested-With: XMLHttpRequest )。
    2. 检查请求方法(GET/POST)和参数是否正确。
    3. 该密钥接口本身可能也有防爬措施,需要先完成某种验证。

6.3 请求返回403/401错误

即使Token生成正确,请求也可能被拒绝。

  • Token过期 :Token很可能有时效性。检查 payload 中是否包含时间戳 timestamp ,服务器端会验证这个时间戳是否在允许的时间窗口内(如±5分钟)。确保你的系统时间准确,并且时间戳格式(秒还是毫秒)与服务器一致。
  • Nonce重复 :随机数 nonce 可能被服务器记录,短时间内重复使用会被拒绝。确保每次请求都生成新的随机数。
  • 请求签名范围 :有些签名算法不仅对参数签名,还会把HTTP方法、请求路径甚至部分请求头也纳入签名计算。你需要仔细阅读JS代码,看签名原始串是如何组装的。

6.4 execjs执行环境问题

  • execjs._exceptions.ProgramError :通常是JS代码本身在执行环境中有语法错误或缺少依赖。
    • 将你的JS代码先在Node.js命令行或浏览器Console中单独运行测试,确保无误。
    • 检查是否遗漏了某些全局变量或函数的定义。可能需要将依赖的JS库(如CryptoJS的完整源码)一起打包进执行上下文。
  • 性能问题 execjs 调用JS的速度较慢。如果每个请求都需要调用,会成为性能瓶颈。尽量将算法完全移植到Python,或者将 execjs 上下文创建一次并重复使用。

6.5 代码混淆与反调试

高级的网站会使用代码混淆工具(如 obfuscator )和反调试技术。

  • 反调试 :在开发者工具打开时,网站可能会检测到并跳转或卡死。可以尝试使用 setTimeout 断点、 debugger; 语句绕过,或者使用无头浏览器工具如 Puppeteer 进行调试。
  • 代码混淆 :变量名变成 a b c ,逻辑被分割成无数个小函数。这需要极大的耐心。策略是:
    1. 先格式化代码。
    2. 搜索关键常量字符串(如加密算法名 AES encrypt ,或API端点 /api/getData )。
    3. 找到引用这些字符串的函数,逐步理清逻辑。动态调试(断点)在此刻比静态阅读更有效,因为你可以直接观察真实的数据流。

整个逆向分析的过程,就像一场耐心的狩猎。你需要细心观察、大胆假设、小心验证。每一次成功破解,不仅是为了拿到数据,更是对自身技术能力的一次锤炼。最后再次强调,技术是一把双刃剑,请在法律和道德允许的范围内,合理、克制地使用这些知识。控制你的爬虫速度,尊重网站的服务器压力,这才是技术人应有的素养。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值