Shopee x-sap-ri签名参数解析:从抓包到Python复现的完整指南

📅 发布时间:2026/10/14 4:31:08
Shopee x-sap-ri签名参数解析:从抓包到Python复现的完整指南
简介面向移动安全与逆向工程学习者这份源码包以Shopee应用中的x-sap-ri参数为分析对象完整拆解52位字符的生成逻辑。前8位字符由时间戳经多层位运算转换而来后44位则依赖一组特定随机数序列并包含固定位模式资源同时梳理了干扰项识别与剔除的可行方案。分析过程借助InsCode快马平台搭建ARM64模拟环境在一个受控的虚拟设备中完成动态验证并通过自上而下、由果溯因、关键函数分析三种方法论进行交叉印证帮助读者建立可迁移的逆向分析框架。资源共3个文件涵盖可运行的inscode工程配置、记录完整思路的HTML分析笔记以及配套gitignore文件压缩包仅6KB轻量且便于快速查阅核心结论。目前已有138人学习适合电商端安全研究、爬虫对抗及算法分析领域的中高级开发者深入学习。1. Shopee x-sap-ri参数分析为什么这个请求头让自动化脚本集体翻车做电商接口自动化时最玄学的往往不是接口逻辑而是请求头里那个x-sap-ri。同一个请求浏览器里点一次能通换成Python脚本原样复制却拿到403/406同一个账号第一次签名能用过五分钟同样的算法却不再被认可。这个问题我断断续续踩了大半年最后才把它的生成链路、参与字段和校验窗口彻底摸清。x-sap-ri是Shopee在Web端请求中携带的签名参数通常和另外两个会话类请求头一起出现直接决定了服务端认不认这次请求。这篇文章会用一套可复现的分析流程讲清楚如何在JS源码里定位生成函数、用Python复现以及那些让新手集体翻车的边界情况。适合正在做接口调试、自动化测试和数据采集验证的开发者尤其适合被签名头卡住、却不知道怎么下手的人。2. 先搞清楚x-sap-ri是什么字段拆解与抓包定位在动手写代码之前先把x-sap-ri放到整个请求链路里看。它不是一个独立变量而是和一批请求头协同生成的。新手最常见的做法是拿到一个x-sap-ri就往代码里塞这样做往往坚持不过5分钟——因为哪怕同一台机器下一次请求的x-sap-ri也变了。先建立整体认知后面定位算法时才不会迷失方向。2.1 x-sap-ri在Shopee请求头里的真实位置抓包先看这三件套打开抓包工具在请求详情里搜索x-sap-ri通常能看到它与相邻的两个请求头一起出现。为方便描述下面把它们称为x-sap-sec与x-sap-ses。三者的变化频率和用途并不相同简单对比如下。请求头典型形态变化频率对签名的可能作用x-sap-ribase64或十六进制串长度常见40~64每次请求都变请求体签名服务端做完整性校验x-sap-sec较短十六进制串登录后基本固定可能作为签名密钥或密钥的一部分x-sap-ses会话标识串登录后固定标识会话可能拼进待签名串表格里的“可能作用”是分析初期的假设真正结论要等后面拿到生成函数才能坐实。但分清楚三者变化频率非常关键它会直接告诉你x-sap-ri的动态部分到底来自时间戳还是来自某个随机的会话token。比如x-sap-sec登录后固定那它大概率是密钥材料x-sap-ses固定说明签名与具体登录态绑定换账号后旧签名全部失效。抓包这一步不要只盯着单个请求看。我一般会同时看三组请求登录前的一个无鉴权请求、登录后的详情请求、以及一个写操作请求。对比这三次请求里的x-sap-ri前缀和长度可以快速判断算法是否区分“读请求”和“写请求”。很多签名方案对GET与POST采用不同的拼接规则这一步差别直接在抓包里就能发现。请求列表太杂时给抓包工具加一个过滤器只保留包含x-sap-ri的请求。多数抓包工具支持按请求头名称过滤例如在过滤条件里填x-sap-ri列表会迅速收敛到真正需要分析的请求。把命中的请求导出成HAR文件后续离线分析用JSON解析比手动截图记录高效得多。2.2 不同端上的x-sap-ri形态差异PC网页、App与内置WebViewx-sap-ri在PC浏览器、独立App、内嵌WebView里的来源完全不同这个差异决定了你该去哪一层断点。PC网页端的前端代码是直接可读的在浏览器DevTools的Sources面板里搜索字段名就能定位适合作为第一战场。Android/iOS独立App则麻烦得多签名可能由原生层甚至so库生成抓到包也未必能追踪到调用栈。内嵌WebView介于两者之间签名逻辑在前端H5里但外壳App可能注入额外参数。端定位难度常见入手点PC浏览器低DevTools直接搜JS源码断点Android App高反编译与抓包对照可能涉及so库iOS App高抓包结合调用栈分析WebView中前端资源包内搜索代码做第一轮分析时建议优先选择PC网页端。原因很实际除了源码可读它还能利用DevTools的Overrides功能直接修改JS文件把x-sap-ri的生成函数替换成自己写的日志函数几秒钟就能看到入参。App端一条条反编译成本高得多等你在Web端把算法族确认下来再迁移也不迟。2.3 从参数命名看参与字段x-sap-ri与x-sap-sec、x-sap-ses的分工这三个头的命名有规律可循ri更像请求体摘要sec更像密钥ses更像会话标识。用黑盒方法可以验证这个猜测。第一次请求记录三个头的值然后无痕重开窗口重新登录再发一次相同请求。如果x-sap-ses变了而x-sap-sec没变说明ses与账号登录态绑定sec可能来自前端固定配置或设备信息。接下来是更细粒度的差分实验。保持URL不变只修改请求体里的一个数字观察x-sap-ri是否变化再保持请求体不变只修改查询参数观察是否变化。这个过程可以帮助建立“输入字段集合”。不要一次改两个变量否则无法确定每个字段是否参与签名。把实验结果记成一张表后面复现签名时就是你的校验清单。常见做法里x-sap-ri至少会拼入请求路径和请求体的哈希再叠加上时间戳或会话标识。如果你发现一个接口的x-sap-ri完全不变那有两种可能一是这个接口压根不校验签名二是算法里的动态部分来自一个长有效期token。判断方法很简单过10分钟再请求一次看是否变化。另外要注意x-sap-ses如果过期即使签名算法完全正确请求也会失败。遇到这类情况先刷新会话不要急着怀疑签名函数。3. 从JS源码定位x-sap-ri生成算法断点、Hook与差分对照第2章解决了“它是什么”的问题这一章解决“它在哪生成”。核心思路不是去读压缩后的全部JS而是用断点和Hook把生成函数从调用栈里“钓”出来再用差分输入确认参与字段。整个过程不需要一开始就理解算法细节先找到函数再逐步解剖。3.1 用断点锁定发请求的调用栈先在抓包工具里确认目标接口的URL特征。以某商品详情接口为例URL里通常包含类似/api/v1/item的路径。在DevTools的Sources面板中可以给fetch或XMLHttpRequest统一打一个条件断点。更方便的做法是直接在Console里注入一段包装脚本只在目标URL出现时暂停执行。// 在浏览器Console中注入给fetch加条件断点 (function () { const nativeFetch window.fetch; window.fetch function (...args) { const url typeof args[0] string ? args[0] : args[0].url; if (url url.includes(/api/v1/item)) { console.trace([x-sap-ri], url); debugger; } return nativeFetch.apply(this, args); }; })();这段代码做了三件事。第一保存原生fetch引用避免递归调用。第二检查请求URL是否包含目标路径包含时才触发断点。第三console.trace打印调用栈debugger暂停执行让调用栈面板保留现场。要注意的是部分平台的请求库内部实现不是fetch而是XMLHttpRequest对这类请求需要再补一段对XMLHttpRequest.open的包装判断URL后再发送时暂停。一旦断点停下在右侧Call Stack面板里找到最靠近顶层的那几帧。往上找能看到一个函数它的返回值最后被塞进了headers对象。双击这一帧DevTools会跳到对应的源码位置接下来就可以在这个位置附近找x-sap-ri的赋值语句。3.2 在JS源码里搜x-sap-ri找到赋值点的两个常见位置拿到调用栈后更直接的路线是在DevTools的Sources面板里全局搜索x-sap-ri。按CtrlShiftF各浏览器快捷键略有差异输入字段名等待搜索结果。通常结果会落到两个位置之一。第一个位置是统一请求拦截器形态类似// 请求拦截器里统一添加签名头 t.headers[x-sap-ri] Object(ret.calcSapRi)(t, e); t.headers[x-sap-ses] e.sessionId;第二个位置是独立的签名工具模块截获到的函数名可能是calc、sign、genSign之类。进入这个模块后重点看它读取了哪些变量。压缩后的代码里变量名只有一个字母不要急着读逻辑先把函数体复制出来格式化然后搜索Date.now、JSON.stringify、sha256、HmacSHA256这些关键词。出现这些词的位置就是签名算法的核心。3.3 用差分输入判断哪些字段参与了签名拿到函数体后可能仍然看不清入参。这时候用差分输入来反向验证。所谓差分就是控制变量只改变一个输入观察输出变不变。如果x-sap-ri随之改变说明该字段一定进入了签名如果不变则说明它没参与或被固定值替代。实际操作时我一般会在DevTools里修改请求体后重新发送记下x-sap-ri的变化。更高效的方法是用Python脚本把多组抓包样本整理成列表批处理对比。# 差分样本对比判断签名是否受查询参数影响 samples [ {query: {itemid: 1}, x_sap_ri: a1b2c3}, {query: {itemid: 2}, x_sap_ri: d4e5f6}, {query: {itemid: 1}, x_sap_ri: a1b2c3}, ] # 若仅itemid不同的两条签名不同而相同itemid在不同时间签名相同 # 说明itemid参与签名且该样本内没有时间因子这段脚本的逻辑说明第一条与第三条除时间外完全一致若签名一致且两次抓包间隔几秒说明签名里没有毫秒级时间戳第一条与第二条仅itemid不同若签名不同说明itemid参与计算。这个实验能快速判断动态来源。如果三组样本全部不同那时间戳大概率参与需要在样本里补齐抓包时间字段再做一次对照。注意差分实验需要保证请求体大小、路径、Cookie都不变否则任何变化都可能来自无关变量结论不可信。我在最初分析时犯过这个错同时改了请求体和UA结果差异来源根本无法归因。后来每次只改一个字段才把参与字段清单弄准。提示差分实验每次只改一个字段改完记录一次结果再改下一个。同时改两个变量等于没做实验。3.4 从输出长度反推哈希算法16字节与32字节的分水岭确定了参与字段后下一步是确认哈希算法族。x-sap-ri通常是编码后的字符串要么是十六进制要么是Base64。先把抓包值还原成原始字节数一下长度。import base64 sap_ri 从抓包里复制的一串值 padded sap_ri * (-len(sap_ri) % 4) # 补齐base64填充 raw base64.b64decode(padded) print(len(raw)) # 输出16、32或64这段代码先补齐Base64的等号填充再解码成字节。16字节通常对应MD5或HmacMD532字节一般对应SHA-256或HMAC-SHA-25664字节则可能是SHA-512。拿到长度后回到源码搜索算法名称基本能锁定。如果是Hmac类算法代码里会出现一个密钥变量如果是普通哈希那么拼接串里必须有某种固定盐值否则任何人拿到算法都能伪造。真实场景里以HmacSHA256最为常见因为它同时满足“不可伪造”和“实现简单”两个条件。4. 用Python复现x-sap-ri生成逻辑最小可运行代码与参数说明定位到算法并确认参与字段后就可以在Python里复现了。复现的核心不是把JS翻译成Python而是把“拼接顺序、编码方式、序列化规则”这三件事完全对齐。很多人卡在这一步往往不是算法不明而是Query参数排序、JSON序列化空格、时间戳单位这些细节点没对齐。下面给出一套我常用的最小骨架。4.1 最小可运行的Python签名生成代码# x-sap-ri 签名生成示例按常见实现简化的骨架 import base64 import hashlib import hmac import json from typing import Dict, Optional def _sha256_hex(data: str) - str: return hashlib.sha256(data.encode(utf-8)).hexdigest() def generate_x_sap_ri( method: str, path: str, query: Dict[str, str], body: Dict[str, object], timestamp_ms: int, secret: str, session_id: Optional[str] None, ) - str: # 1. 查询参数按键名升序排序避免歧义 canonical_query .join( f{k}{query[k]} for k in sorted(query) ) # 2. 请求体做紧凑JSON序列化并计算SHA256 body_hash _sha256_hex( json.dumps(body, separators(,, :), sort_keysTrue) ) # 3. 拼接待签名原文顺序需与线上JS一致 raw \n.join( [ method.upper(), path, canonical_query, body_hash, str(timestamp_ms), session_id or , ] ) # 4. HMAC-SHA256 后做 Base64URL 编码去掉末尾等号 digest hmac.new( secret.encode(utf-8), raw.encode(utf-8), hashlib.sha256, ).digest() return base64.urlsafe_b64encode(digest).decode(utf-8).rstrip()这段代码的逻辑说明第1步把查询参数按键名排序后拼接避免URL中参数顺序变化导致签名不一致第2步先对请求体做紧凑JSON序列化再哈希sort_keys保证字典键顺序固定第3步按method、path、query、body_hash、timestamp、session_id的顺序拼接换行符作为分隔第4步用HMAC-SHA256对原文做运算输出Base64URL编码。真实线上算法不一定完全按这个顺序但它覆盖了最常见的参与维度和编码细节你可以对照自己的样本逐行调整。调用示例# 调用示例 sign generate_x_sap_ri( methodGET, path/api/v1/item, query{itemid: 1}, body{}, timestamp_ms1700000000000, secret从x-sap-sec或JS常量中取到的密钥, session_id从Cookie中解析的会话标识, ) print(sign)调用示例说明GET请求没有请求体body传空字典timestamp_ms必须与请求头中实际发送的时间戳一致不能动态生成否则服务端比对时间窗口会直接拒绝。注意真实线上算法的拼接顺序以你断点拿到的源码为准。代码骨架是分析工具不是万能模板。4.2 参数说明与排序规则一个都不能错复现签名时最容易出错的是参数的归一化规则。下面这张表列出我每次核对签名时都要过一遍的清单。参数来源常见错误核对方法method请求方法忘了转大写统一upperpath不含域名的路径把query也拼进path只取问号前部分queryURL查询串未排序按键名字典序body_hash请求体JSON序列化带了多余空格separators(,, :) 且sort_keystimestamp_ms请求头里的时间戳用time.time()动态生成取抓包样本原值时对比secretx-sap-sec或JS常量混入换行符strip后使用session_idCookie或缓存缺失或None不参与时传空串这张表的核心逻辑是“归一化”同一份输入在JS里和Python里必须产生完全一致的字节。JSON序列化尤其容易翻车Python默认在冒号后加空格而JS压缩后的代码通常不带空格一旦不一致body_hash对不上签名就不可能正确。建议在实现后先拿5组抓包样本做离线路测全部通过再进入联调。4.3 签名生成后请求仍被拒按状态码做第一轮排错算法看似正确请求还是失败时先看HTTP状态码。不同状态码指向的问题完全不同不要盲目改算法。状态码高频原因排查方向401会话过期或Cookie缺失刷新session_id确认Cookie与签名是否同一会话403签名校验失败或权限不足对比生成值与抓包值核对签名算法406请求头或编码不被接受检查Content-Type、Accept头、请求体格式429请求频率超限降低频率检查是否有会话级频控拿到403时不必立刻怀疑算法先做一件事把生成脚本里用到的各个输入打印出来和抓包请求里的原始字节逐一比对。我会在本地保存一份JSON日志记录每次请求的method、path、query、body_hash、timestamp、secret前几位然后与抓包样本对比。多数被迫调整的案例都是某个字段多了换行或少了排序导致的。5. x-sap-ri分析避坑指南5个高频踩坑点这个章节写实际分析中容易踩的坑。每条都按“现象→原因→解决”来写都是我在模拟项目X和其他环境里反复遇到过的。5.1 现象一单接口能过批量请求被风控单次请求带着生成的x-sap-ri能拿到正常响应一旦脚本进入循环批量请求同一接口很快就出现403甚至429。原因在于签名正确只是“请求被认可”的必要条件不是充分条件服务端还会结合Cookie、IP、请求频率、会话活跃度做综合判断。x-sap-ri保证请求未被篡改但解决不了频率问题。解决思路是先降低并发到一个保守值观察多长时间内稳定同时确认批量请求之间是否复用同一个session_id如果复用说明服务端能通过会话时间判断出异常。一次一个变量地调不要同时换IP、降并发、改签名。5.2 现象二同一组参数隔5分钟再签名结果就变了同一份method、path、query、body在本地按同一算法重新生成签名与5分钟前抓包里的值不同。原因有两种可能一是签名里夹了毫秒级时间戳而这个时间戳你在生成时没有沿用原值而是用了当前时间二是签名里引用了某个短期有效的token比如从响应头或Cookie里动态读取的会话因子。解决方法是回到抓包样本把当时实际发送的时间戳字段提取出来与样本中的x-sap-ri做离线对照如果离线完全一致再在真实请求里使用对齐后的时间戳。这个坑最容易被误判成算法错误其实算法没错错的是时间基准。5.3 现象三直接复制浏览器请求头到脚本里无效从DevTools复制出来的完整请求头里明明包含x-sap-ri但原样粘贴到Python requests里却返回403。原因是x-sap-ri与Cookie、User-Agent等客户端标识是绑定的单独复制签名值但其他头部或会话状态变了服务端做一致性校验时就会拒绝。解决方向不是把整个请求头都搬过来而是把签名生成函数一起搬过来并保持User-Agent与会话一致。我一般会先用浏览器原始会话在脚本里做一次最小复现成功后再逐步替换成动态生成的头这样能明确区分是签名问题还是会话问题。5.4 现象四本地生成的签名值比抓包短一截按Base64URL编码输出后发现生成的串比抓包值短了一截。原因通常是参与签名的字段比预期多或者是哈希算法族判断错误。比如你以为只用HMAC-SHA256实际线上是HMAC-SHA256之后再接过一个固定前缀或者编码前先做了压缩。解决方法是回到源码搜签名函数里对输出的二次加工逻辑例如拼接前缀或再次编码。同时用第3章的字节长度统计重新核对原始输出长度如果长度刚好差一个固定串优先怀疑前缀如果长度完全不对优先怀疑算法族。5.5 现象五请求头里多了个空格直接401生成的x-sap-ri本身正确但代码在组装请求头时不小心在字段值前后引入了空格或把Header名的size写乱了结果返回401。原因在于HTTP头部解析对空白严格尤其签名值参与服务端校验时尾部空格会让值不匹配。解决方法是统一对所有Header键值做strip并保持与线上完全一致的风格。这个坑看起来低级却最容易在代码重构时出现。我会在request封装里加一个字典解析函数把headers.items()的每个key和value都strip掉并写一个单元测试固定住。6. 把x-sap-ri分析固化成本地回归用例一次请求的三种验证分析完成不代表一劳永逸线上JS一旦更新算法随时可能调整。我最后会再做一件事把抓包样本变成自动化回归用例确保每次改动代码后都能快速知道签名生成是否还成立。6.1 离线回归把抓包样本变成测试用例准备一个JSON文件每个样本记录method、path、query、body、timestamp、secret、expected。然后用测试代码批量验证。import json with open(sapri_cases.json, r, encodingutf-8) as f: cases json.load(f) for case in cases: actual generate_x_sap_ri( methodcase[method], pathcase[path], querycase[query], bodycase[body], timestamp_mscase[timestamp_ms], secretcase[secret], session_idcase.get(session_id), ) assert actual case[expected], fcase failed: {case[path]}这段代码从JSON文件读取抓包样本逐条调用签名生成函数并与抓包值做等值断言。一旦线上算法更新导致签名输出变化最先挂掉的就是这些用例。把这些用例放进CI哪怕只是本地脚本也能在改动后几秒内定位问题。6.2 在线对照用低风险接口验证签名是否被后验校验离线回归验证的是“你生成的签名和抓包一致”但服务端是否真正校验又是另一回事。可以用一个低风险的公开详情接口做在线对照分别带签名和不带签名发送两次正常频率的请求观察响应差异。如果无签名请求返回相同数据说明该接口对签名是弱校验可以暂时不接入生成逻辑如果无签名请求直接异常则必须保留签名链。注意只选只读类、无副作用的接口不要拿登录、下单、修改类接口做这种实验。我在模拟项目X里踩过最深的一次就是跳过差分实验直接猜算法结果把拼接顺序改了一整轮才发现问题白白浪费一周。现在我的习惯是每次拿到新的x-sap-ri样本先存成回归用例再写算法。算法再复杂验证链路稳定后面调试就不慌。希望帮到你。本文还有配套的精品资源点击获取