逆向电商平台加密签名参数:以FastMoss的fm-sign为例
2026/7/30 16:37:20 网站建设 项目流程

1. 项目概述:当电商数据爬取遇上加密参数

最近在做一个电商数据分析的项目,目标平台是FastMoss。这个平台上有大量店铺和商品数据,对于市场研究来说是个宝库。但和很多现代电商网站一样,FastMoss在保护其核心数据接口上下了不少功夫,其中最让人头疼的就是那个叫fm-sign的加密参数。无论是查询店铺列表、搜索商品,还是获取店铺详情,几乎所有重要的API请求里都带着这个参数。没有它,服务器直接返回403或者一个空数据包,爬虫工作瞬间陷入僵局。

这个fm-sign参数,本质上是一个签名(Signature)。它的作用是验证请求的合法性,确保请求来自其官方前端,而非我们这样的“外来者”。它通常由请求的URL、时间戳、请求参数以及一个只有服务器才知道的密钥(Secret Key)通过特定算法(如HMAC-SHA256)计算得出。前端在发起请求前,会实时计算这个签名并附在请求头或参数里;后端收到请求后,会用同样的逻辑再算一遍进行比对。如果对不上,请求就被视为无效或恶意请求。这种机制在如今的Web开发中非常普遍,尤其是在涉及敏感数据或需要防刷的接口上,比如电商、社交、金融等平台。

逆向破解这个参数,目的不是为了“攻击”网站,而是为了在合规的数据采集框架下(例如,遵守Robots协议,控制请求频率,不进行恶意刷取),能够自动化地获取公开数据进行分析。这对于竞品分析、价格监控、市场趋势洞察等商业智能应用至关重要。如果你也遇到过类似的加密墙,或者对Web逆向、JavaScript调试感兴趣,那么接下来的内容应该能给你提供一套清晰的实战思路。整个过程就像一场数字侦探游戏,我们需要在前端浩如烟海的JavaScript代码中,找到生成那个关键“密码”的“钥匙”和“算法”。

2. 逆向工程的核心思路与准备工作

逆向一个前端加密参数,核心目标就是找到生成这个参数的JavaScript代码逻辑,并能在我们自己的环境(如Node.js、Python)中复现它。这听起来像大海捞针,但只要有正确的工具和方法,路径是清晰的。我们的思路可以概括为“定位、分析、提取、复现”四个步骤。

首先,我们需要强大的工具。浏览器开发者工具(Chrome DevTools)是我们的主战场。其中,“网络”(Network)面板用于捕获和分析HTTP请求,特别是观察fm-sign出现在哪个请求的哪个位置(是Headers里的X-Sign还是Query Parameters里的sign?)。而“源代码”(Sources)面板和“控制台”(Console)面板则是我们深入JavaScript腹地的关键。此外,我会强烈推荐使用一些专门用于反混淆和调试的浏览器扩展,比如“ReRes”(可以本地替换线上JS文件,方便我们修改和调试)、“EditThisCookie”(管理Cookie,有时密钥或种子藏在Cookie里)。对于复杂的、经过混淆和压缩的代码,一个能格式化(Pretty Print)JS代码的IDE或在线工具也必不可少。

在开始逆向之前,我们必须先理解常见的加密签名生成模式。这能帮助我们在看到一堆乱码般的代码时,快速识别出关键部分。最常见的模式包括:

  1. 基于时间戳的哈希sign = md5(timestamp + secret_key)sign = hmac_sha256(path + '?' + sorted_params, secret_key)。这里的secret_key可能是一个固定字符串,也可能是从某个接口或Cookie动态获取的。
  2. 包含请求指纹:签名可能不仅包含参数,还会混入一些浏览器指纹,如User-Agent的某部分、屏幕分辨率等,增加逆向难度。
  3. 多层加密与混淆:先对参数进行某种编码(如Base64),再进行哈希,或者使用非标准的自定义哈希算法。
  4. WebAssembly(Wasm):最高级别的防御,将核心算法编译成Wasm模块,JavaScript只负责调用。这大大增加了逆向难度,需要分析Wasm二进制文件。

对于FastMoss的fm-sign,我们首先假设它属于第一种或第二种常见模式。我们的突破口,就是找到那个执行签名计算的JavaScript函数。

注意:所有逆向分析工作都应仅针对公开可访问的前端JavaScript代码进行。切勿尝试破解服务器端私有密钥、入侵服务器或进行任何违反法律法规和网站服务条款的操作。本文讨论的技术仅用于安全研究和学习目的。

3. 定位加密函数的关键技巧

打开FastMoss网站,并进入开发者工具的“网络”面板。清空记录,然后进行一次会触发携带fm-sign参数的请求操作,比如点击“店铺排行”或进行搜索。在请求列表中,找到目标请求(通常是XHR/Fetch类型),点击查看其“标头”(Headers)和“负载”(Payload/Payload)。确认fm-sign的位置和值。

接下来是最关键的一步:搜索。由于代码被压缩,函数名和变量名都是单字母,直接搜索“fm-sign”或“sign”可能无果。我们有几种搜索策略:

  1. 搜索参数名:在“源代码”面板中,使用全局搜索(Ctrl+Shift+F),搜索fm-sign这个字符串。如果它作为键(key)出现在参数对象里,比如{‘fm-sign’: ‘xxx’},我们就能找到构建请求参数的地方。
  2. 搜索加密算法特征:搜索MD5SHAHMACencryptsignCryptoJS(一个常用的前端加密库)等关键词。即使变量名被混淆,这些算法库的函数名或字符串常量可能保留。
  3. 搜索固定字符串或数字:观察fm-sign的值,如果它总是以固定字符开头或结尾,或者长度固定,可以尝试搜索这些特征值的一部分(需转义)。但通常签名值每次请求都变,此法不常用。
  4. XHR/Fetch断点:在“源代码”面板,找到“事件监听器断点”(Event Listener Breakpoints),展开“XHR”或“Fetch”,勾选“readystatechange”或“fetch”。然后再次触发请求,代码会在发起网络请求前暂停。此时调用栈(Call Stack)会显示是哪个函数发起的请求,我们可以一步步往回追溯,找到生成签名的代码。
  5. Hook技术:这是一种更高级且高效的方法。在控制台(Console)中,我们可以注入代码来“钩住”(Hook)关键函数。例如,我们可以重写XMLHttpRequest.prototype.sendwindow.fetch方法,在请求发出前,打印出它的参数,从而定位到添加fm-sign的代码位置。
// 示例:Hook fetch 方法 (function() { var originalFetch = window.fetch; window.fetch = function() { console.log(‘Fetch called with arguments:‘, arguments); // 可以在这里下断点 debugger; return originalFetch.apply(this, arguments); }; })();

执行上述代码后,再触发请求,控制台会打印出详细的调用信息。查看arguments,如果请求的配置对象(如Request或init参数)中包含fm-sign,我们就能看到它被添加的时刻和上下文。

在我的实战中,通过Hookfetch并配合“XHR断点”,我成功将范围缩小到了一个名为main.xxxxxx.js的压缩文件中的某几行代码。格式化代码后,发现了一个关键函数调用:o = n(‘a1b2‘).default.sign(t, e)。这里的n看起来像是Webpack的模块加载器,‘a1b2‘是模块ID,.sign是导出的签名方法,te是参数。

4. 深入分析加密算法与参数构成

找到疑似签名函数后,下一步就是深入这个模块,理解它的输入和输出。我们需要找到模块‘a1b2‘的定义。在格式化后的JS文件中,搜索‘a1b2‘:“a1b2“:,通常能在Webpack的模块定义数组或对象中找到它。

果然,我找到了类似这样的代码块:

/* 模块ID a1b2 */ (function(module, exports, __webpack_require__) { “use strict“; Object.defineProperty(exports, “__esModule“, { value: true }); exports.default = void 0; var _cryptoJs = __webpack_require__(“c3d4“); var _default = { sign: function(params, timestamp) { // 1. 对参数对象进行按键名排序 var sortedKeys = Object.keys(params).sort(); var strToSign = ““; for (var i = 0; i < sortedKeys.length; i++) { var key = sortedKeys[i]; if (params[key] !== null && params[key] !== undefined) { strToSign += key + ‘=‘ + params[key] + ‘&‘; } } // 去除末尾的‘&‘ if (strToSign.endsWith(‘&‘)) { strToSign = strToSign.slice(0, -1); } // 2. 拼接上时间戳和路径(假设从上下文中获取了apiPath) var apiPath = window.__API_PATH__ || “/api/v1/search“; // 示例 var rawSignStr = apiPath + ‘?‘ + strToSign + ‘&t=‘ + timestamp; // 3. 使用HMAC-SHA256进行加密,密钥似乎是一个固定值 var secret = “fastmoss_secure_key_2023“; // **注意:这是示例,真实密钥不同** var hash = _cryptoJs.HmacSHA256(rawSignStr, secret); // 4. 将哈希结果转换为十六进制字符串 var sign = hash.toString(_cryptoJs.enc.Hex); return sign; } }; exports.default = _default; })

分析这段代码(已做简化脱敏),我们可以清晰地看到fm-sign的生成逻辑:

  1. 参数排序与拼接:将请求的查询参数(GET)或表单参数(POST)对象,按照键名(key)的字母顺序进行排序。然后拼接成key1=value1&key2=value2的标准格式。这一步是为了保证无论前端以何种顺序传递参数,后端都能以同样的规则生成签名进行比对。
  2. 构造待签名字符串:将API请求的路径(Path)与排序后的参数字符串、以及一个时间戳t拼接起来。格式类似于:/api/v1/shop/list?keyword=abc&page=1&t=1646389471123。这个t参数通常就是当前时间戳(毫秒级),也会作为独立参数发送到服务器。
  3. HMAC-SHA256加密:使用CryptoJS.HmacSHA256算法,用某个密钥(secret)对上面构造的字符串进行加密,生成一个哈希值。这里的secret是关键中的关键。在示例中它被硬编码在代码里,但在真实场景中,它可能被混淆、拆分成多个部分、或从首次请求的响应中动态获取。
  4. 输出格式化:将二进制哈希结果转换为十六进制(Hex)字符串,这个字符串就是最终的fm-sign值。

至此,算法逻辑已经明朗。但还有一个重大问题:那个secret密钥,在混淆的代码里可能不是明文的“fastmoss_secure_key_2023“。它可能被编码(如Base64)、被拆分成数组然后拼接、或者隐藏在某个全局变量、Cookie甚至第一次请求的响应数据里。我们需要继续追踪这个secret的来源。

5. 密钥追踪与算法复现

追踪密钥需要耐心和细致的代码阅读。在找到的签名模块附近,搜索secretkeysalt等词汇。查看_cryptoJs.HmacSHA256的第二个参数是什么。它可能是一个变量,比如var s = ‘f‘ + ‘a‘ + ‘s‘ + ‘t‘ + ...,通过字符串拼接来隐藏;也可能是一个函数调用的返回值。

在我的案例中,通过进一步搜索和调试,发现secret是通过一个名为getSecurityKey的函数获取的。这个函数会先检查localStorage中是否有缓存,如果没有,则发起一个特定的初始化请求(例如GET /api/init)来获取一个临时的token,然后用这个token和客户端的一些固定信息(如App版本号)通过一个简单的变换得到最终的secret。这个token有过期时间,过期后需要重新获取。

实操心得:遇到这种动态密钥,不要慌。通常第一次获取密钥的接口本身是不需要签名的,或者使用更简单的签名(甚至没有)。我们可以先用浏览器正常访问网站,在开发者工具的“网络”面板中找到这个初始化请求,记录下返回的token。然后,在我们的爬虫脚本中,第一步就是模拟这个初始化请求,拿到token,计算出当前的secret。这样,后续所有需要签名的请求就都能处理了。

搞清楚了算法和密钥来源,我们就可以开始用Python(以requestshashlib库为例)或Node.js来复现这个签名逻辑了。以下是Python版本的复现代码示例:

import hashlib import hmac import time import urllib.parse def generate_fm_sign(api_path: str, params: dict, secret_key: str) -> (str, int): “““ 生成FastMoss风格的fm-sign签名。 参数: api_path: API路径,如 ‘/api/v1/shop/list‘ params: 请求参数字典 secret_key: 从初始化接口获取的密钥 返回: (signature, timestamp) 元组 “““ # 1. 生成当前时间戳(毫秒) timestamp = int(time.time() * 1000) # 将时间戳加入参数 all_params = params.copy() all_params[‘t‘] = timestamp # 2. 对参数按键进行排序并拼接 sorted_keys = sorted(all_params.keys()) param_list = [] for key in sorted_keys: value = all_params[key] if value is not None: # 注意:值可能需要URL编码,取决于原前端如何处理 # 通常拼接时用原始值,但发送请求时需要编码。这里按原前端逻辑来。 param_list.append(f“{key}={value}“) query_string = “&“.join(param_list) # 3. 构造待签名字符串 string_to_sign = f“{api_path}?{query_string}“ # 4. 使用HMAC-SHA256计算签名 # 注意:secret_key需要转换为bytes, string_to_sign也需要转换为bytes secret_bytes = secret_key.encode(‘utf-8‘) message_bytes = string_to_sign.encode(‘utf-8‘) hmac_obj = hmac.new(secret_bytes, message_bytes, hashlib.sha256) signature = hmac_obj.hexdigest() # 获取十六进制哈希值 return signature, timestamp # 使用示例 if __name__ == “__main__“: # 假设的密钥(实际应从初始化接口获取) my_secret = “dynamic_secret_from_init_api“ # 请求参数 my_params = { “keyword“: “蓝牙耳机“, “page“: 1, “pageSize“: 20 } api_path = “/api/v1/search“ sign, ts = generate_fm_sign(api_path, my_params, my_secret) print(f“生成的签名: {sign}“) print(f“使用的时间戳: {ts}“) # 最终请求URL应构造为:f“{base_url}{api_path}?keyword=蓝牙耳机&page=1&pageSize=20&t={ts}&fm-sign={sign}“

这段代码完全复现了我们在前端JavaScript中分析的逻辑。关键在于确保每一步的细节都与前端一致:参数排序规则(通常是字典序)、空值处理、字符串拼接格式(是否包含?,是否在末尾有多余的&)、以及编码问题。

6. 完整爬虫流程集成与注意事项

将签名生成函数集成到一个完整的爬虫流程中,通常包含以下步骤:

  1. 会话维持:使用requests.Session()来保持Cookie,模拟浏览器会话。许多网站的初始化token或后续请求会依赖Session Cookie。
  2. 获取动态密钥:首先访问初始化接口(如/api/init/config),从响应中提取生成secret所需的信息(如tokennonce等),并按照前端逻辑计算出secret_key。将这个secret_key和可能的token过期时间缓存起来。
  3. 构造请求:为每个需要签名的API准备参数。
  4. 生成签名:调用generate_fm_sign函数,传入API路径、参数和secret_key,得到fm-signtimestamp
  5. 发送请求:将timestamp作为t参数,fm-sign作为fm-sign参数,与其他原始参数一并发送请求。注意参数放在查询字符串(GET)还是请求体(POST)中,需与前端保持一致。
  6. 处理响应:解析返回的JSON数据。
  7. 错误处理与重试:如果返回签名错误(如HTTP 403或特定的错误码),可能是token过期。此时应清除缓存的secret_key,回到步骤2重新获取。
  8. 请求频率控制:严格遵守伦理爬虫规范,在请求间添加随机延时(如time.sleep(random.uniform(1, 3))),避免对目标服务器造成压力。

常见问题与排查技巧实录

  • 问题1:生成的签名总是无效。
    • 排查:这是最常见的问题。使用对比法。用浏览器正常操作一次,在开发者工具中记录下精确的请求URL(包括所有参数和值)以及请求发出的时间。同时,用你的脚本在同一时刻(时间戳相同)用相同的参数计算签名。然后逐字对比两个待签名字符串(string_to_sign)是否完全一致。特别注意:参数值的类型(数字1vs 字符串“1“)、空格、编码(%20vs+)、参数的顺序、甚至路径末尾的斜杠(/api/pathvs/api/path/)。
  • 问题2:初始化接口也需要签名,陷入死循环。
    • 排查:仔细检查初始化接口的请求。它可能使用一种更简单的签名方式(如简单的MD5),或者根本不带签名,而是依靠其他验证(如HTTP Referer头)。也可能密钥是固定的,硬编码在另一个JS文件里,无需初始化请求。
  • 问题3:算法看起来更复杂,似乎不是标准HMAC。
    • 排查:如果搜索CryptoJS没找到,可能网站使用了自定义的加密函数或WebAssembly。此时HookMath.randomDate.nowArray.prototype.join等基础函数可能有助于定位。或者,可以考虑使用自动化工具如PuppeteerSelenium直接运行浏览器环境,让JavaScript自然执行并获取结果,但这会牺牲一些效率和资源。
  • 问题4:成功几次后,IP被封锁。
    • 应对:这是反爬策略。需要添加代理IP池,并进一步降低请求频率,模拟更真实的人类行为(如随机滑动鼠标、随机等待时间)。检查请求头是否完整模拟了浏览器(User-Agent,Accept,Accept-Language,Referer等)。

逆向fm-sign这样的参数,是一个典型的Web逆向工程案例。它考验的是耐心、细心和对HTTP协议及前端JavaScript运行原理的理解。整个过程从模糊的猜测开始,通过工具定位、逻辑分析、代码还原,最终得到一个清晰、可复现的算法模型。这种能力在需要与复杂前端接口打交道的自动化工作中非常有用。最后要再次强调,技术应当用在正当的领域,在获取数据时务必尊重网站的robots.txt规则,控制请求速率,避免对他人服务造成干扰。

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

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

立即咨询