news 2026/9/19 21:25:07

Shopee接口逆向:x-sap-Ri与x-sap-Sec签名生成全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Shopee接口逆向:x-sap-Ri与x-sap-Sec签名生成全解析

做电商平台数据采集的人,这两年应该都能明显感觉到,Shopee的详情页接口已经不是当年随手一抓就能返回数据的时代了。打开商品详情页,随便点一个请求,Headers里都会带上两个非常显眼的参数:x-sap-Rix-sap-Sec。第一次看到的人基本都会一脸懵——这两个 Header 既不是标准的sign,也不是常见的token,名字完全是内部约定的风格。

我最早接触这两兄弟是在排查商品价格和库存接口返回 403 的时候。服务端对缺少这两个请求头的请求直接拒绝,而且返回的响应体没有任何业务数据。当时第一反应是“是不是账号风控了”,后来把浏览器里正常发出去的请求完整复制一遍,加上x-sap-Rix-sap-Sec之后,接口立刻恢复正常。这就说明问题了——这两个 Header 是服务端校验请求合法性的核心凭证,属于典型的前端加密请求头,必须在页面内部动态生成。

这文章就把我完整调试这两个参数的思路、断点定位、加密逻辑拆解和最终用 Node.js 复现的过程写清楚,给同样卡在 Shopee 详情页逆向上的朋友一个可以直接落地的参考。

1. 先说清楚:x-sap-Ri和x-sap-Sec到底在防什么

不理解反爬方的意图,逆向的时候就容易走弯路。这两个请求头实际干的事,可以从它们的出现位置和请求特征上一层层拆开看。

1.1 请求头的大致形态与定位

在浏览器开发者工具里打开任意一个 Shopee 商品详情页,切到 Network 面板,刷新页面后找/api/v4/item/get这个接口(不同地区域名可能有差异,但路径基本一致)。点开 Headers,拉到 Request Headers 区域,能看到这样几个关键项:

x-api-source: pc x-sap-ri: 30f0e8d8f8f8e8c8... x-sap-sec: 1a2b3c4d x-sap-web-version: 2a1a5e5

我第一次看到x-sap-ri是一长串十六进制字符串时,第一反应是 MD5。等打断点进去看生成过程后才发现,这是一个经过多层拼接后做的消息摘要结果。x-sap-sec则短得多,通常是一个 8 到 16 位的十六进制串,看起来像是某种摘要的截断或者版本标记。这两个头经常组合出现,缺一个服务端都会返回异常。

有必要说一下,这两个参数不是固定不变的。同一个页面,第一次加载和第二次刷新,生成的x-sap-ri完全不同;即使同一份商品数据,用不同浏览器环境访问,结果也不一致。这说明生成逻辑里掺入了随机数、时间戳或环境指纹类的动态因子。

1.2 服务端校验的大致流程

从请求到达服务端后的校验顺序推测,x-sap-sec更像是一个快速过滤字段——服务端可能先用它做版本或算法标识判断,如果不匹配就直接拒绝,不进入耗时的完整校验。而x-sap-ri则是真正验证请求完整性的核心签名。校验逻辑大概有三层:

  • 第一层:Header 是否缺失,缺失直接 403。
  • 第二层:x-sap-sec格式和取值是否在允许范围内,用于打击非浏览器环境发出的请求。
  • 第三层:x-sap-ri是否与请求路径、查询参数、时间戳等要素匹配,防止请求被篡改或重放。

这种设计在大型电商平台里非常常见,相当于“快速通道检查 + 深度签名验证”的双层结构。理解这一点对后续逆向很有帮助——我们要找的两个参数,可能来自同一套加密逻辑,也可能来自不同的模块。实际调试下来,它们的生成是独立的,但x-sap-sec的生成过程里会读取x-sap-ri,二者在请求头里拼装时才走到一起。

1.3 为什么直接搜 Hook 不到

不少做爬虫的朋友习惯用hook关键词的方式定位加密参数,比如在 Console 里重写XMLHttpRequest或者fetch,把请求头里的值打印出来。但 Shopee 这类的签名参数,直接 hook 浏览器原生对象往往只能看到最终结果,看不到生成过程。

原因在于,前端代码里可能不是直接给XMLHttpRequest.setRequestHeader传这两个字段,而是先构造一个 header 对象,再经过统一请求库封装,最后才发给浏览器。你在 hook 点能拿到值,但拿不到函数调用栈和算法上下文。这也是为什么下文所有分析都围绕开发者工具的“搜索 + 断点 + 调用栈”三件套来做,而不是依赖全局 hook。

提示:先定位请求头在 JS 代码里被赋值的语句,再去打断点看生成处,比在 hook 里大海捞针要高效得多。

2. 定位加密源头:从接口到断点的完整链路

拿到要逆的参数名字,下一步就是找到它们是在哪个 JS 文件、哪个函数里被计算出来的。很多人卡在这一步,因为 Shopee 的 JS 文件做了混淆和分包加载,直接按文件名找几乎找不到。正确路线是:搜索字符串 → 定位赋值语句 → 回溯调用栈 → 找到核心加密函数

2.1 用全局搜索快速锁定出现位置

打开 DevTools,在 Network 面板里右键发起详情页请求的接口名,选择“Search”或者直接在 Sources 面板里按Ctrl+Shift+F打开全局搜索。输入x-sap-ri(注意区分大小写,如果搜不到,可以试试小写x-sap-ri或者去掉连字符的变体),搜索结果会列出所有出现该字符串的 JS 文件和行号。

我这里实际操作时,第一次搜索出现了多个结果,主要是以下两类:

  • 字段定义处:例如headers: { 'x-sap-ri': generateRi(), 'x-sap-sec': generateSec() }
  • 字符串拼接处:某些 logger 或埋点逻辑里也会打印请求头,容易干扰判断

第一遍搜索结果里,优先找赋值语句集中在请求库封装层的那个文件。通常是一个几百 KB 的 vendor 或 runtime 文件,加载时机在页面初始化阶段。点进去,格式化代码,然后在格式化后的代码里再搜索一次x-sap-ri

这里有个小技巧:格式化之前先记住原始 JS 文件的行号,格式化之后行号会变,再搜索一次反而更容易定位。因为格式化后的代码会把压缩在一行的内容拆开,搜索命中会显示具体逻辑,而不是一整行超长代码。

2.2 在赋值语句上打断点并触发请求

找到类似下面这样的代码段:

t.headers = t.headers || {}, t.headers["x-sap-ri"] = (0, n.generateRi)(), t.headers["x-sap-sec"] = (0, n.generateSec)(), t.headers["x-api-source"] = "pc", t.headers["x-sap-web-version"] = "2a1a5e5"

x-sap-ri这一行左侧点击加断点。然后刷新页面,代码会在这个位置暂停。这时在 Scope 面板里能看到当前的t对象,也就是即将发送的请求配置,headers里这两个字段的值还没有生成。

点击 DevTools 里的“Step into next function call”,会跳进generateRi的实现。如果generateRi是 webpack 模块导出函数,跳转后能看到一个独立的函数体。这里就是我们需要重点分析的起点。

2.3 从生成函数回溯整个调用链

进入生成函数后,不要急着看算法,先在函数内部打断点,重新触发,观察参数都是什么。generateRi通常接收一个配置对象,里面包含urlmethodparamsdata等信息。实际操作中,它的签名大致是这样的:

function generateRi(e) { var t = e.url; var n = e.method; var r = e.params; var o = e.data; // 关键加密逻辑 }

其中e.url是完整的接口路径,e.params是查询参数对象,e.data是 POST 请求体。这些信息会被拼进一个字符串里,然后做摘要运算。加密函数往往依赖一个独立的工具函数,比如getSignSaltgetTimestamp。这些工具函数可能定义在更底层的模块里,调用栈里能看到一串嵌套关系。

分析到这里,大致可以画出调用链:

  • 请求库封装层组装 headers
  • generateRi接收请求配置并提取关键要素
  • generateRi内部调用摘要工具函数(MD5/SHA 系列)
  • 摘要结果经过字符串处理和大小写转换,最终作为 header 值返回

这个链路理清楚后,剩下的事就是逐行读懂这些函数。

3. x-sap-Ri与x-sap-Sec的加密逻辑拆解

整个逆向过程里,最核心的就是把这两个参数的生成代码读懂。下面按我的调试顺序,把关键逻辑和识别方法展开讲。

3.1 x-sap-Ri:拼接、加盐、摘要

通过断点观察,x-sap-Ri的生成逻辑大致如下:

先把请求参数里的关键字段按固定顺序拼成一个长字符串。常见的拼接要素包括:

  • 请求路径(不包含域名部分,例如/api/v4/item/get
  • 查询参数,按 key 排序后拼接(例如itemid=123&shopid=456
  • POST 请求体(如有)
  • 当前时间戳(通常精确到秒)
  • 一段固定或半固定的盐值(salt)

然后对这个字符串做消息摘要。我在实际调试中看到的是类似 MD5 的 32 位十六进制输出,但并不能武断地认为只有 MD5。判断摘要算法的常见方法是在 Console 里调用CryptoJS.MD5CryptoJS.SHA256做对照测试,看输出是否与页面生成的一致。

下面是一段为便于讲解而简化的示例代码,实际项目中盐值和拼接格式会有变化,但整体骨架类似:

function generateRi(e) { var t = e.url; var n = e.params || {}; var r = e.data || {}; var i = Math.floor(Date.now() / 1000); // 秒级时间戳 var o = t + "?" + sortParams(n) + "&data=" + JSON.stringify(r) + "&t=" + i + "&salt=SAP_RI_2024"; return md5(o).toUpperCase(); }

这里要重点看的是salt值从哪来。不少平台的盐值写在某个配置模块里,可能是一段纯字符串,也可能是从接口下发的。Shopee 的盐值通常就藏在一个独立模块中,搜索saltsignsecret之类的关键词可以找到。

注意:x-sap-Ri生成时用的时间戳,如果与请求头里的时间字段不一致,服务端会判定签名过期。所以复现时一定要保证所有时间相关字段统一。

3.2 x-sap-Sec:版本标记还是独立签名

x-sap-Sec的生成相对复杂一点。它不是一个单纯的摘要,而是带有版本信息的加密串。从断点里看到的形态类似于"1" + 随机数 + 校验码,或者某些情况下等于一个固定前缀加x-sap-Ri的碎片。

我最开始以为x-sap-Secx-sap-Ri的再次摘要,但对比日志发现两者并没有直接的转换关系。观察生成函数发现,x-sap-Sec内部会调用另一个模块读取设备指纹信息,包括但不限于:

  • navigator.userAgent的哈希
  • navigator.language
  • screen.widthscreen.height
  • document.cookie的部分值
  • 插件列表或 Canvas 指纹

这些信息经过拼接后,再与x-sap-Ri的值做一次运算,最终截取定长字符串作为x-sap-Sec。换句话说,x-sap-Sec更像是一个“环境可信度标记”,用来验证当前请求是否来自真实浏览器环境。

复现x-sap-Sec的难点不在算法本身,而在环境参数的获取。用 Node.js 发送请求时,需要手动构造一个足够接近真实浏览器的环境信息。好消息是,服务端对x-sap-Sec的校验往往只关注固定字段和摘要格式,不会要求所有指纹位点完全一致。

3.3 算法识别的三种方法

面对一堆压缩混淆的代码,怎么快速判断它用了什么加密?我总结了三招:

第一招:观察输出长度。32 位十六进制大概率是 MD5;40 位是 SHA1;64 位是 SHA256。如果输出里既有数字又有 a-f 字母,基本确定是 hex 编码。

第二招:在函数内部搜索CryptoJScreateHashmd5sha等关键词。压缩后的代码虽然变量名被替换,但方法名通常还是保留的,因为这些是库的公开 API。

第三招:断点进入加密函数后,直接在 Console 里手动构造一个输入值,调用同一个函数看输出,再用本地工具(如openssl dgst -md5)算一次,对比结果。一致的话算法就确认了。

下表是我在实际调试中常用的判断对照:

特征可能算法备注
32位hex,字母在a-fMD5最常见
40位hexSHA1较少见
64位hexSHA256逐渐增多
输出含g-z等非hexBase64编码后的摘要需反查原始字节
短串(8-16位)截断摘要或CRC类注意函数截取逻辑

4. 实战复现:用Node.js还原请求头生成流程

理清加密逻辑后,就可以脱离浏览器,在 Node.js 环境里把生成过程复刻一遍。做到这一步,之前所有的逆向分析才算真正落地。

4.1 抽取核心JS片段

在 DevTools 里把generateRigenerateSec相关的函数完整复制出来。这里注意不要只复制主函数,它依赖的工具函数也要一起抽取,比如sortParamsmd5getDeviceFingerprint

一种比较省事的方式是:在 Sources 面板里把整个包含加密模块的文件保存到本地,在本地文件里搜索函数名,手动删掉无关代码,只保留核心逻辑。如果文件依赖很多 webpack 模块,直接用webpack的模块导出函数是更好的方式,但手工抽取对单个加密函数来说完全够用。

抽取后的代码大致长这样:

const crypto = require('crypto'); function md5(str) { return crypto.createHash('md5').update(str, 'utf8').digest('hex'); } function sortParams(params) { return Object.keys(params).sort().map(k => `${k}=${params[k]}`).join('&'); } function generateRi(url, params, data) { const timestamp = Math.floor(Date.now() / 1000); const raw = `${url}?${sortParams(params)}&data=${JSON.stringify(data)}&t=${timestamp}&salt=SAP_RI_2024`; return md5(raw).toUpperCase(); } function generateSec(ri, userAgent) { const uaHash = md5(userAgent); const base = `${ri}_${uaHash}_${ri.length}`; return md5(base).slice(0, 16); }

这是简化后的逻辑,仅供演示整体思路。实际抽取的代码里如果引用了windowdocumentnavigator等浏览器对象,就需要做下一步的补环境处理。

4.2 补环境:在Node里伪造浏览器对象

页面源码里很多工具函数直接访问windowdocument,比如读取 Cookie、获取屏幕尺寸。Node.js 里没有这些对象,最简单的办法是在文件开头做全局变量注入。

global.window = global; global.navigator = { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', platform: 'Win32', language: 'zh-CN', languages: ['zh-CN', 'en'], cookieEnabled: true }; global.document = { cookie: '', referrer: 'https://shopee.sg/', title: '', documentElement: { style: {} }, createElement: function() { return {}; } }; global.screen = { width: 1920, height: 1080, colorDepth: 24 }; global.location = { href: 'https://shopee.sg/product/123', hostname: 'shopee.sg', protocol: 'https:' };

补环境的原则是“缺什么补什么、报错缺哪个对象再补哪个”。不要一上来就补一大堆,容易引入不存在的属性反而干扰后续调试。运行时如果报xxx is not defined,就在代码里搜索xxx出现的位置,按需补充。

4.3 验证:生成的签名能否通过接口校验

环境补齐后,写一个小脚本测试生成的请求头是否有效。先请求详情页首页拿关键 Cookie(比如SPC_FSPC_TFSPC_ECD),再带上生成的x-sap-Rix-sap-Sec请求商品详情接口。

const axios = require('axios'); const url = '/api/v4/item/get'; const params = { itemid: 123456, shopid: 789012 }; const ri = generateRi(url, params, {}); const sec = generateSec(ri, global.navigator.userAgent); const response = await axios.get('https://shopee.sg' + url, { params, headers: { 'x-api-source': 'pc', 'x-sap-ri': ri, 'x-sap-sec': sec, 'x-sap-web-version': '2a1a5e5', 'User-Agent': global.navigator.userAgent, 'Cookie': cookieStr, 'Referer': 'https://shopee.sg/category/11036082' } }); if (response.data && response.data.data) { console.log('请求成功,item数据正常返回'); } else { console.log('请求失败,响应:', response.data); }

第一次跑大概率会失败,常见原因是 Cookie 缺失或签名里的时间戳与服务器时间不一致。如果返回{"error": 400}或者{"error": 403},不要慌,把响应文本打出来看具体提示,再回到加密函数里找原因。

提示:Shopee 对 Cookie 的依赖不亚于签名参数。脚本里cookieStr必须来自真实浏览器登录(或访问)过同区域站点后的值。仅靠签名参数无法完全绕过风控。

4.4 请求频率与并发控制

签名参数能通过之后,还有一个散户踩坑最多的地方:请求频率。我曾经在测试阶段对同一商品 ID 连续请求 20 次,第 10 次以后开始出现滑块验证码。Shopee 的详情接口存在明显的 QPS 限制,不同 IP 段、不同账号状态阈值也不同。

实践里比较稳妥的做法是:

  • 单商品轮询间隔至少 2 秒
  • 用固定 Cookie 池轮换请求,单账号单会话的请求频率不要过高
  • 出现验证码时立刻停掉当前 IP,切换出口 IP,等几分钟再继续
  • 尽量通过商品搜索接口拿列表,再进详情页拿数据,模拟真实用户路径

5. 调试现场:高频问题与排查技巧

这部分内容是我在实际复现过程中踩过的坑汇总。逆向这种事,思路对了是水到渠成,思路偏了真的能把人卡到怀疑人生。

5.1 断点挂起导致请求超时

打断点调试的时候,经常遇到页面卡住,请求迟迟不发出。这是断点位置太靠前的正常现象。如果断点打在了请求库封装层,函数执行流程会停在发送前,浏览器不会立即发出网络请求,一直等到你放行断点才继续。

所以调试时要注意:放行前先观察函数调用栈和参数内容,改代码、刷新页面、清空缓存这类操作要在放行之后再执行。否则页面会一直停在挂起状态,容易误判为请求被拦截。

5.2 复现时出现环境检测报错

抽取代码到 Node.js 后,运行时经常报navigator is not definedwindow is not defined。解决办法就是我上面说的补环境。但有一点容易被忽略:某些检测代码不是直接访问window,而是通过globalThis或自执行函数传参。这时候补环境的位置很关键,必须在引入被检测代码之前完成注入。

还有一种情况是代码里用了Object.defineProperty重写某个浏览器属性,比如定义不可枚举的navigator.webdriver等。复现时如果发现环境检测特别严格,可以在补环境阶段对这类属性做特殊处理。

5.3 签名过期与重放校验

调试过程中发现,同一组签名在一两分钟内有效,过几分钟再用就返回 400 了。这说明服务端对签名里的时间戳做了时效校验。解决办法是脚本每次请求前都重新生成签名,不要缓存risec的值。

重放攻击防护方面,x-sap-Ri绑定了请求参数和时间戳,同一组签名无法用于其他请求路径,这本身就在限制重放。所以如果你的脚本因为重试机制反复用同样的签名打同一个接口,被拒绝是正常的。每个请求都调用生成函数,是必须养成的习惯。

5.4 常见问题速查表

现象可能原因解决方案
返回 403缺少x-sap-Ri或拼写大小写错误检查 header 字段名和值格式
返回 400签名时效过期或参数与签名不匹配重新生成签名,检查拼接顺序
返回 200 但 data 为空Cookie 缺失或会话过期更新 Cookie,重新访问目标页面
出现验证码请求频率过高或 IP 被风控降频,切换出口 IP,等待解封
Node 报window is not defined未补环境或补的位置不对在引入业务代码前注入全局变量
生成的签名与页面不一致盐值或拼接格式理解有误加日志对比每一次拼接的中间结果

5.5 一个最容易被忽略的细节

x-sap-Ri的生成函数里,如果请求体是 POST 且字段顺序不固定,那么 JSON 序列化之前的 key 顺序也会影响签名结果。有些实现会先把对象 key 排序再序列化,有些则直接按对象原有顺序。复现时一定要在浏览器里打印出真正参与拼接的字符串,并在 Node 端传入完全相同的字符串做计算。

我一开始在某个 POST 接口上反复失败,后来发现就是JSON.stringify序列化顺序不一致导致的。这个问题特别隐蔽,因为它不报错,只是签名对不上。定位方法是在浏览器里把拼接的原始字符串打日志,然后在 Node 端把同样的字符串打印出来,一行行比对。

另外,签名里的大写和小写转换也要注意。页面生成的x-sap-ri是纯大写十六进制,但x-sap-sec可能是小写混合。如果在代码里统一做了.toUpperCase(),很容易把sec的值也转换掉,导致校验不通过。正确做法是严格按照页面代码的处理方式,不额外做大小写统一。

最后的实操体会

这两个参数我前前后后大概花了一个多星期才完全跑通。回头再看,最大的感悟是:逆向这种工作,耐心比技巧重要。断点、搜索、调用栈这些工具每个人都会用,难的是在混淆代码里保持清晰的思路,一步步缩小范围,而不是一上来就想着全局 hook 一举拿下。

如果你正准备啃 Shopee 的签名参数,我的建议是先从小接口练手,比如商品搜索建议接口或者分类页接口,这些请求的参数结构简单,拼接逻辑更容易看清楚。等把x-sap-Ri的生成逻辑摸透了,再上详情页这个复杂度更高的接口,会顺手很多。

还有一点,自己写脚本验证签名时,记得把每次请求的 headers、响应状态码、响应体完整记录下来。遇到问题回头看日志,比反复猜测要高效得多。采集这件事本身就是个长期工程,反爬策略会更新,签名参数也会变化,但分析思路和调试手段是通用的,掌握这套方法论,遇到新的加密参数也不会慌。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 21:23:55

KimiCode 跑 Kimi K3 长程 Agent,Base URL 填 TaoToken 兼容地址

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 21:20:19

SLG大地图表层渲染实战:数据分层 + Tilemap + Shader性能优化

做SLG大地图,最容易被低估的就是地表渲染这一层的复杂度。我接手过几个策略项目,大地图格子数动不动就是百万级,一开始团队习惯性地用每块地一个Sprite的方式堆,结果小米手机开个全屏地图直接变成暖手宝,帧率掉到个位数…

作者头像 李华
网站建设 2026/9/19 21:19:16

Chrome无法正常使用?从安装到崩溃的完整故障排查与修复指南

Chrome用着用着突然就废了,这是很多人的真实体验。我自己就处理过大大小小几十台机器的Chrome故障,从装不上、白屏、闪退,到扩展装不了、网页打不开、标签页一直转圈,什么问题都见过。今天就系统地把Google Chrome无法正常使用的各…

作者头像 李华