Shopee签名机制逆向工程:从JavaScript算法到Python复现的完整实战
2026/7/15 12:47:36 网站建设 项目流程

1. 项目概述:为什么我们要研究Shopee的签名机制?

做电商数据采集的朋友,对Shopee这个东南亚巨头平台一定不陌生。无论是选品分析、价格监控还是竞品研究,都需要稳定、高效地获取其站点上的商品、店铺和搜索数据。然而,和许多现代大型电商平台一样,Shopee为了保护其服务器资源和数据安全,部署了相当复杂的反爬虫机制,其中最关键的一环就是其请求签名机制。直接使用简单的requests库发送请求,十有八九会吃到“403 Forbidden”或者返回一堆乱码。这个项目,就是一次彻底的“外科手术式”逆向工程,目标不是简单地绕过一两个参数,而是深入理解Shopee(以网页端为例)如何为每个请求生成唯一的、有时效性的“身份证”——签名,并基于此构建一套可以长期稳定运行的自动化采集方案。这不仅仅是写几行代码,更是一场与平台风控工程师的智力博弈,理解他们的设计思路,才能找到最优雅、最持久的应对策略。

2. 核心思路拆解:从黑盒到白盒的逆向路径

面对一个未知的签名算法,最忌讳的就是一头扎进代码里胡乱尝试。一个清晰的逆向路径能事半功倍。我的核心思路可以概括为“由外而内,动静结合”。

2.1 逆向工程的基本方法论

逆向工程不是瞎猜,它遵循一套科学的方法论。对于Web端的签名,尤其是JavaScript实现的,我们通常采用“动态分析为主,静态分析为辅”的策略。动态分析指的是在浏览器真实环境中运行目标网页,通过开发者工具监控网络请求、调用堆栈、内存变量变化,从而观察签名生成的完整流程。静态分析则是在拿到关键的JavaScript文件后,对其进行代码格式化、反混淆、逻辑梳理,理解其算法原理。两者结合,动态分析帮我们快速定位关键函数和参数,静态分析帮我们彻底理解其内部逻辑,为后续的代码复现打下坚实基础。

2.2 Shopee签名机制的特点预判

在开始动手前,根据经验和对Shopee这类大型平台的分析,我们可以对其签名机制做一些合理预判:

  1. 前端生成:签名极大概率是在浏览器端(JavaScript)生成的,因为需要用到浏览器的环境信息(如User-Agent、屏幕分辨率等)或本地计算能力。
  2. 参数绑定:签名不会只对一个固定字符串加密,它必然与本次请求的某些特征强绑定,例如请求的URL路径、查询参数(Query String)、请求体(Body)、甚至时间戳。
  3. 时效性:为了防止签名被复用,通常会引入时间戳(timestamp)或随机数(nonce)作为签名因子之一,使得每个签名在短时间内(如几秒或几分钟)内唯一有效。
  4. 算法复杂度:可能使用常见的哈希算法(如MD5、SHA-256),也可能使用HMAC,或者更复杂的自定义算法。大型平台为了增加逆向难度,常会对JavaScript代码进行混淆和压缩。
  5. 环境检测:签名过程可能隐式或显式地依赖浏览器环境,比如检查windowdocument对象是否存在,或执行一些只有浏览器中才有效的JavaScript代码。

基于这些预判,我们的逆向工作就有了明确的搜索方向。

3. 实操环境准备与关键请求定位

工欲善其事,必先利其器。逆向工作对工具链的要求比较高。

3.1 工具选型与配置

  1. 抓包工具CharlesFiddler是必备的。它们能拦截和查看所有HTTP/HTTPS流量。我更喜欢Charles,因为它的界面更清晰,重发请求(Compose)和断点调试(Breakpoints)功能非常强大。配置时,记得在电脑和手机(如果需要抓APP包)上安装并信任Charles的根证书。
  2. 浏览器开发者工具:Chrome DevTools 或 Edge DevTools 是主战场。重点关注Network(网络)面板和Sources(源代码)面板。在Network面板中,要勾选“Preserve log”(保留日志)并禁用缓存(Disable cache)。
  3. 反混淆与调试工具:浏览器自带的调试器通常够用。但对于高度混淆的代码,可以借助一些浏览器插件或在线工具进行初步的格式化,但最关键的还是靠人脑去理解逻辑。“Pretty print”(美化代码)功能是Sources面板里的神器,一定要会用。
  4. 编程环境:Python 是首选,配合requestsexecjs(用于执行JavaScript)、node.js本地环境等库。准备好一个代码编辑器,如VS Code。

3.2 定位签名参数与入口

打开Shopee的网页,例如搜索页面https://shopee.sg/search?keyword=shoes。在Charles或浏览器Network面板中,仔细查看请求头(Headers)。

很快你会发现,关键的接口请求(通常是XHR类型),其请求头里会包含一些特殊的字段,这正是我们搜索片段中提到的sap-rix-sap。不同的版本或地区,字段名可能略有差异,但思路一致。例如,你可能会看到:

  • x-api-source
  • x-requested-with
  • 以及最重要的:x-sap-rix-sap-signature(或类似名称)。

这些字段的值通常是一长串看似随机的字母数字组合,它们就是签名的产物。我们的目标就是找出生成这些值的JavaScript函数。

操作技巧:在Network面板中,找到携带这些签名头的请求,右键点击,选择“Copy -> Copy as cURL”。然后粘贴到一个文本编辑器中,你可以清晰地看到所有头信息。这有助于你理解请求的全貌。

4. 动态调试:追踪签名函数的执行过程

找到了签名参数,下一步就是找到生成它们的代码。

4.1 启用浏览器调试器进行堆栈追踪

  1. 在Network面板中,点击那个携带签名头的请求。
  2. 切换到Initiator标签页。这里显示了是哪个脚本文件发起了这个网络请求。点击旁边那个行号,它会直接跳转到Sources面板对应的代码位置。这通常是我们寻找签名函数的第一个入口。
  3. 更通用的方法是使用“XHR/Fetch Breakpoints”。在Sources面板的XHR Breakpoints区域,点击“+”号,添加一个断点条件,可以填写请求URL的一部分,比如“/api/”。这样,当任何包含“/api/”的请求发起时,浏览器就会自动暂停(Break),此时调用堆栈(Call Stack)里就会显示出导致这个请求的所有函数调用链。

4.2 关键函数定位与参数分析

当断点触发,JavaScript执行暂停后:

  1. 观察Call Stack面板。你会看到一长串函数调用。从下往上(或从上往下)看,寻找那些看起来与网络请求、参数处理、加密相关的函数名。例如,可能会看到signencryptgetHeadersaxios.interceptors.request.use(如果Shopee使用axios)等。
  2. 点击Call Stack中的不同函数,在右侧代码区查看其源码。即使代码被混淆,你也可以通过观察函数内部的变量赋值、参数传递来推断其作用。
  3. 重点关注:在疑似签名生成的函数附近,使用“Step Over” (F10)“Step Into” (F11)等按钮单步执行。同时,将鼠标悬停在变量上,或在Console面板中直接输入变量名,查看其当前值。你的目标是找到最终赋值给x-sap-rix-sap-signature的那个变量或表达式。
  4. 记录关键信息:一旦找到生成签名的核心函数,记下:
    • 函数名(混淆后的也行)。
    • 该函数所在的JavaScript文件路径或名称。
    • 函数的输入参数是什么?(通常是URL、请求方法、请求数据、时间戳等)。
    • 函数的输出是什么?(就是签名值)。

实操心得:混淆后的代码变量名可能是abcten等单字母,非常难读。这时候不要慌,多利用断点和单步执行,观察这些变量在关键节点(如进入函数时、经过某个运算后)的值变化,结合上下文(比如它后面被拼接成URL或放入请求头)来反推它的含义。例如,一个变量在经过一系列操作后被放入了x-sap-ri请求头,那它很可能就是ri参数。

5. 静态分析与算法还原

通过动态调试,我们定位到了核心函数。接下来,就需要把这个函数的逻辑完整地“搬”到我们的Python环境中。

5.1 提取并格式化关键JavaScript代码

  1. 在Sources面板,找到包含签名函数的那个.js文件。它可能是一个很大的vendor.xxxx.jsapp.xxxx.js
  2. 选中关键函数及其周边相关的依赖函数(比如它调用的其他加密函数、工具函数),右键点击,选择“Save as…”保存到本地。
  3. 用代码编辑器打开,如果代码是压缩成一行的,先用浏览器的“Pretty Print”功能格式化,或者使用在线JS格式化工具,让代码变得可读。

5.2 分析算法逻辑与依赖

格式化后的代码虽然变量名依然混乱,但结构清晰了。你需要像侦探一样梳理逻辑:

  1. 找出入口函数:确认哪个函数是我们动态调试时找到的签名生成函数。
  2. 理清输入输出:明确这个函数接收哪些参数,返回什么。
  3. 拆解计算步骤:一步步分析函数内部做了什么。常见的操作包括:
    • 字符串拼接:将URL路径、查询参数、时间戳、固定字符串等按特定顺序拼接成一个待签名的原始字符串。
    • 参数排序:可能对URL的查询参数(Query Params)按照字典序排序后再拼接。
    • 哈希运算:调用CryptoJS.MD5CryptoJS.HmacSHA256或浏览器原生的SubtleCryptoAPI。
    • Base64编码:将哈希结果进行Base64编码。
    • 引入随机数:生成一个随机字符串作为ri(request id)。
  4. 识别外部依赖:注意函数内部是否使用了浏览器特有的对象(如windownavigatordocument)或全局变量。这些在Node.js或execjs环境中可能不存在,需要我们在复现时模拟或替换。

5.3 使用Python复现签名算法

这是最具挑战性也最有成就感的一步。目标是写一个Python函数,给定相同的输入,能输出与浏览器完全一致的签名。

import hashlib import hmac import base64 import time import json from urllib.parse import urlparse, parse_qs, urlencode class ShopeeSigner: def __init__(self): # 这里可能需要初始化一些从JS中提取的固定密钥或盐值 self.secret_key = "从JS中分析出的一个固定值,可能藏在代码里" self.app_version = "某个版本号" self.platform = "web" def generate_ri(self): """生成随机的request id,模仿JS中的实现""" import uuid # 有时可能是时间戳+随机数的一种组合,需要根据JS代码确定 return str(uuid.uuid4()).replace('-', '') def generate_signature(self, method, url, body=None, timestamp=None): """ 核心签名函数 :param method: HTTP方法,如 'GET', 'POST' :param url: 完整的请求URL :param body: POST请求的JSON体,字典格式 :param timestamp: 时间戳(秒级),如果为None则使用当前时间 :return: 包含 ri 和 signature 的字典 """ if timestamp is None: timestamp = int(time.time()) # 1. 解析URL,获取路径和查询参数 parsed_url = urlparse(url) path = parsed_url.path # 例如 '/api/v4/item/get' query_params = parse_qs(parsed_url.query) # 2. 处理查询参数:可能需要排序、过滤空值、特定格式编码 # 根据JS逻辑,这里可能需要按key排序后,拼接成 `key1=value1&key2=value2` 的形式 sorted_query_str = "" if query_params: # 注意:parse_qs返回的值是列表,需要根据实际情况处理(如取第一个值) sorted_items = sorted(query_params.items(), key=lambda x: x[0]) sorted_query_str = '&'.join([f"{k}={v[0]}" for k, v in sorted_items if v[0]]) # 3. 处理请求体 body_str = "" if body and method.upper() == 'POST': # 根据JS逻辑,可能是JSON字符串,并且可能需要按key排序后再字符串化 body_str = json.dumps(body, separators=(',', ':'), sort_keys=True) # 4. 拼接待签名字符串 (这是最关键的一步,顺序和格式必须与JS完全一致) # 以下拼接方式仅为示例,真实逻辑需逆向得出 string_to_sign = f"{method}\n{path}\n{timestamp}\n{sorted_query_str}\n{body_str}\n{self.app_version}" # 或者可能是: string_to_sign = f"{path}?{sorted_query_str}|{timestamp}|{body_str}|{self.secret_key}" # 5. 计算签名 (示例为HMAC-SHA256) # 密钥可能是固定的,也可能是动态生成的 sign_key = self.secret_key.encode('utf-8') message = string_to_sign.encode('utf-8') hmac_digest = hmac.new(sign_key, message, digestmod=hashlib.sha256).digest() signature = base64.b64encode(hmac_digest).decode('utf-8') # 6. 生成ri ri = self.generate_ri() return { "x-sap-ri": ri, "x-sap-signature": signature, "x-timestamp": str(timestamp) # 有时时间戳也需要放在头里 } # 使用示例 signer = ShopeeSigner() headers = signer.generate_signature( method="GET", url="https://shopee.sg/api/v4/search/search_items?by=relevancy&keyword=shoes&limit=50&newest=0&order=desc&page_type=search&scenario=PAGE_GLOBAL_SEARCH&version=2" ) print(headers) # 将返回的headers添加到你的requests请求中

核心难点与技巧:拼接string_to_sign的规则千变万化,是逆向的精髓。你必须通过反复的断点调试,在JS代码执行到计算签名前的那一刻,把参与拼接的所有变量的值都记录下来。然后在Python里用完全相同的顺序、格式(包括换行符\n、分隔符|等)进行拼接。一个空格或顺序的错误都会导致签名无效。最可靠的方法是对比验证:用你的Python函数算一个签名,同时用浏览器发起一次真实请求,对比两个签名是否完全一致。如果不同,就检查拼接逻辑的每一个细节。

6. 构建稳定采集方案

逆向出签名算法只是第一步,要构建稳定的采集系统,还需要考虑更多工程化问题。

6.1 请求头与会话管理

Shopee的签名不是孤立的,它通常需要和其他请求头配合使用。一个完整的请求头可能包括:

  • User-Agent: 模拟一个真实的浏览器。
  • Referer: 设置为当前页面的来源URL。
  • Cookie: 维持会话状态。对于公开数据采集,可能不需要登录态Cookie,但一些基础Cookie(如地区、语言)可能需要。
  • Accept-Language,Accept-Encoding等标准头。
  • 我们逆向得到的x-sap-ri,x-sap-signature,x-timestamp

建议使用requests.Session()来管理会话,它可以自动处理Cookie,并允许你为所有请求设置公共的头部。

import requests session = requests.Session() session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...', 'Accept': 'application/json, text/plain, */*', 'Accept-Language': 'en-US,en;q=0.9', 'Referer': 'https://shopee.sg/', }) signer = ShopeeSigner() def search_shopee(keyword, limit=50): url = f"https://shopee.sg/api/v4/search/search_items?by=relevancy&keyword={keyword}&limit={limit}&newest=0" # 生成签名头 sign_headers = signer.generate_signature(method="GET", url=url) # 更新本次请求的头部 headers = {**session.headers, **sign_headers} try: resp = session.get(url, headers=headers, timeout=10) resp.raise_for_status() # 检查HTTP错误 return resp.json() except requests.exceptions.RequestException as e: print(f"请求失败: {e}") return None

6.2 频率控制与IP代理策略

即使签名正确,过快的请求频率也会触发风控,导致IP被暂时封锁。

  1. 设置合理的延迟:在请求之间使用time.sleep(random.uniform(1, 3))增加随机间隔,模拟人类操作。
  2. 使用代理IP池:对于大规模采集,这是必须的。可以使用付费代理服务,并实现IP自动切换逻辑。确保代理IP的质量(高匿名、低延迟、高可用)。
  3. 错误重试与退避:当请求失败(返回403、429等状态码)时,不应立即重试。实现一个带有指数退避机制的重试逻辑,并更换代理IP。
import time import random from typing import Optional class RobustFetcher: def __init__(self, proxy_pool=None): self.session = requests.Session() self.signer = ShopeeSigner() self.proxy_pool = proxy_pool # 假设这是一个返回代理字典的函数/列表 self.proxy_index = 0 def get_next_proxy(self) -> Optional[dict]: if not self.proxy_pool: return None proxy = self.proxy_pool[self.proxy_index % len(self.proxy_pool)] self.proxy_index += 1 return {'http': proxy, 'https': proxy} def fetch_with_retry(self, url, method='GET', max_retries=3): retries = 0 while retries < max_retries: proxy = self.get_next_proxy() sign_headers = self.signer.generate_signature(method, url) headers = {**self.session.headers, **sign_headers} try: resp = self.session.request(method, url, headers=headers, proxies=proxy, timeout=15) if resp.status_code == 200: return resp.json() elif resp.status_code in [403, 429]: print(f"请求被阻止 (状态码: {resp.status_code}), 更换代理并重试...") time.sleep(2 ** retries + random.random()) # 指数退避 else: resp.raise_for_status() except Exception as e: print(f"请求异常: {e}") retries += 1 time.sleep(random.uniform(2, 5)) # 重试前等待 print(f"重试{max_retries}次后仍失败: {url}") return None

6.3 数据解析与存储

成功获取到数据(通常是JSON格式)后,需要根据业务需求进行解析和清洗。

  • 结构化数据:将JSON中的商品列表、详情等信息提取出来,转换成字典或列表。
  • 数据清洗:处理价格(可能包含货币符号)、库存状态、图片URL等。
  • 持久化存储:根据数据量选择存储方式。小规模测试可以用JSON或CSV文件。生产环境建议使用数据库,如SQLite(轻量)、PostgreSQL或MongoDB(灵活)。
  • 增量更新:设计机制识别新上架或已下架的商品,避免重复采集全部数据。

7. 常见问题排查与维护策略

逆向方案不是一劳永逸的,平台会更新,我们的方案也需要维护。

7.1 签名失效的快速排查流程

当某天发现采集脚本突然全部失败,返回403时,按以下步骤排查:

  1. 手动浏览器验证:首先用浏览器打开目标页面,打开开发者工具,查看一次成功请求的签名头。确认接口本身是否依然可用。
  2. 对比签名参数:用你的Python脚本为同一个URL生成签名,与浏览器请求中的签名值进行对比。如果不一致,说明签名算法可能已变更。
  3. 重新动态调试:重复第4节的步骤,定位新的签名函数。重点关注之前保存的JS文件是否有更新(通过文件哈希或内容对比)。
  4. 检查依赖环境:确认是否引入了新的浏览器环境检测(如Canvas指纹、WebGL指纹)。简单的请求头模拟可能不再足够,需要考虑使用更高级的模拟方式,如playwrightpuppeteer无头浏览器。

7.2 应对算法升级与风控强化

平台升级反爬策略是常态,要有心理和技术准备。

  1. 代码混淆升级:可能从简单的变量名混淆升级为控制流扁平化、字符串加密等。这大大增加了静态分析的难度。此时更需要依赖动态调试,关注函数的输入输出,必要时可以尝试“黑盒调用”,即用execjs直接执行整个混淆后的JS函数片段,而不是完全用Python重写。
  2. 增加环境指纹:除了请求头,平台可能通过JavaScript收集更详细的浏览器指纹(字体、插件、硬件信息等)。对抗此策略,需要使用能高度模拟真实浏览器的工具,如playwright,它可以生成完整的浏览器上下文,包括所有指纹信息。
  3. 人机验证介入:在频繁访问后,可能会弹出滑块验证码(如Akamai, hCaptcha)。这是最棘手的情况。解决方案包括:
    • 降低频率:这是最有效且合规的方法。
    • 验证码识别服务:接入第三方打码平台(成本较高)。
    • 自动化测试工具:尝试用playwright等自动化操作通过简单验证(成功率不稳定且可能违反平台条款)。

7.3 长期维护建议

  1. 模块化设计:将签名生成、请求发送、错误处理、数据解析等模块分离。这样当签名算法变更时,你只需要修改ShopeeSigner这个类。
  2. 监控与告警:为采集脚本设置健康检查。如果连续多次请求失败或返回非预期数据,通过邮件、钉钉、Telegram机器人等方式发送告警。
  3. 数据备份与版本控制:对逆向出的关键JS代码片段和对应的Python实现代码进行版本管理(如Git)。记录每次算法变更的时间和特征,便于未来回溯。
  4. 遵守规则与伦理:明确你的数据采集目的,控制请求频率,避免对目标网站服务器造成过大压力。只采集公开数据,不尝试破解登录、不涉及用户隐私。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询