
2. 这些参数是怎么生成的先从调用栈找线索定位到参数后第一反应不是去搜索关键词而是直接在发起请求的调用栈里找生成源头。浏览器开发者工具的Network面板通常能直接显示JavaScript调用栈但x-sap-Ri和x-sap-Sec这类参数经常不在标准调用栈里显示因为它们可能是通过XMLHttpRequest或fetch的封装层发送的。我的做法是先在Network面板里找到那个带x-sap-Ri和x-sap-Sec的请求右键菜单里选择Show all requests with this URL或者直接在请求详情里查看Initiator列。如果Initiator显示的是某个压缩后的JS文件比如common.min.js那基本可以确定这个文件就是发送请求的封装地方。这时候很多人会直接在Sources面板里搜索x-sap-Ri但搜索出来的结果往往只有两处一处是把参数写进请求头的地方另一处是用到了参数名的地方。真正生成参数值的代码往往在一个更大的函数里它不会直接引用参数名而是通过对象键的方式组装请求头。所以更好的思路是搜索x-sap-Sec因为签名参数通常比标识参数更早生成而且生成逻辑更复杂涉及的辅助函数更多。在Sources面板里用CtrlShiftF全局搜索输入x-sap-Sec找到引用它的文件后先把该文件格式化点击左下角的{}图标再在格式化后的代码里定位到那一行。我建议在定位到引用处之后往上翻几十行找到这个函数定义的起点然后看函数的入参是什么。通常这个入参里有请求的method、url、data等基本信息生成签名的核心逻辑全部依赖这些入参。有一次我在分析时发现x-sap-Sec是在一个叫buildRequest的函数里被生成并写入请求头的而这个buildRequest函数内部先调用了另一个函数getRi()来生成x-sap-Ri再调用了getSec()来生成x-sap-Sec。两个参数的生成逻辑完全独立但都在同一个函数里被组装到headers对象上。这种代码结构在商业站点的前端里非常典型因为职责分离是三年前就开始的普遍工程实践。2.1 参数生成的一个常见路线时间戳、随机数与序列号组合找到生成函数后把格式化后的代码复制出来逐行分析。这一步我强烈建议不要直接在浏览器里看而是粘贴到一个本地编辑器里加上断点日志或者用reDoS方式分析正则可读性更好的代码。SAP系列的动态参数虽然每个站点的实现不完全一样但业界常见的生成路线我归纳为三类时间戳类从Date.now()或者performance.now()取毫秒时间戳做base62或base36编码作为参数值的核心部分。x-sap-Ri在很多实现里就是这种风格长度在20到30个字符之间前后缀可能是固定字符串加随机数的组合。随机数类用Math.random()或者crypto.getRandomValues()生成随机字节再经过编码。这类参数的特征是每次请求都不同而且没有明显的递增规律。x-sap-Sec如果是签名类参数一般不会用纯随机数因为服务端没法验证但如果它是一个与设备指纹相关的参数那随机部分就很可能存在。序列号类用一个全局计数器或者基于页面加载时间的偏移量来生成递增序号。这类参数的特征是短时间内多次请求参数值的末尾会呈现递增趋势。x-sap-Ri有时候包含这类序列号配合时间戳前缀形成唯一标识。我在一次实际的Shopee类站点分析中发现x-sap-Ri的生成逻辑就是时间戳加随机数取当前时间戳后六位拼接一个由Math.random()生成的8位小写字母与数字混合字符串。整体长度固定每个字符都在0-9和a-z范围内。这个规则一旦看破复现起来非常简单直接写一个函数就能生成。而x-sap-Sec的生成逻辑则会复杂很多它一般涉及请求体的规范化、固定密钥串的拼接、哈希计算等步骤。在浏览器端常见的是SHA-256或者SHA-512因为Web Crypto API原生支持这些算法服务端也容易验证。2.2 代码溯源时常用的三种定位技巧如果你在定位过程里卡住了我分享三个实际验证过有效的技巧第一个技巧是断点早于搜索。在Sources面板里找到发起请求的封装函数在函数体的第一行打断点刷新页面触发请求后代码会停在断点位置。这时候在Scope面板里查看当前作用域的所有变量往往能看到x-sap-Ri和x-sap-Sec的初始值然后再沿着赋值语句往上找生成逻辑。这个方法比盲目搜索靠谱因为压缩后的代码里变量名可能都是a、b、c这种搜索关键词根本搜不到。第二个技巧是重放调用。有些站点只在特定交互下才生成动态参数比如点击某个按钮、切换某个Tab。这种情况下直接在Console里调用同一个函数传不同的参数观察参数值的变化规律能快速确认哪部分与时间相关、哪部分与请求内容相关。第三个技巧是改值验证。在断点处手动修改x-sap-Ri的值改成别的合法格式字符串然后放行请求看服务端会不会返回正常的业务数据。如果服务端接受了说明这个参数在本次请求中不是关键校验位可以优先忽略把精力放在x-sap-Sec上。这个方法能帮你快速排除干扰项不过要注意频率别把人家服务端的风控测出来了。3. 实操全过程一个可复现的本地调试Demo我以一个教学目的的本地调试场景来演示完整流程。这里的代码只用于理解动态参数的生成逻辑请勿用于任何未经授权的数据采集行为。环境准备用Node.js 18以上版本安装axios和crypto-js。我们的目标是在本地模拟生成x-sap-Ri和x-sap-Sec并在请求发出前通过拦截器注入到请求头中然后再请求一个测试接口验证流程跑通。3.1 使用拦截器在请求发出前注入动态参数首先在项目目录下创建一个client.js文件初始化axios实例const axios require(axios); const CryptoJS require(crypto-js); const client axios.create({ baseURL: https://your-test-target.com, timeout: 10000, }); function generateRi() { const timestamp Date.now().toString().slice(-6); const randomPart Math.random().toString(36).slice(2, 10); return timestamp randomPart; } function generateSec(method, url, body, ri) { const sortedBody body ? JSON.stringify(body) : ; const raw [method.toUpperCase(), url, sortedBody, ri, your-fixed-secret-key].join(|); return CryptoJS.SHA256(raw).toString(CryptoJS.enc.Hex); } client.interceptors.request.use((config) { const ri generateRi(); const sec generateSec(config.method, config.url, config.data, ri); config.headers[x-sap-Ri] ri; config.headers[x-sap-Sec] sec; return config; }, (error) Promise.reject(error));这里generateSec函数里我故意用一个simple的拼接方式实际逆向出来的密钥串和拼接规则可能更复杂但思路是完全一致的把请求的关键信息组合成一个字符串再用哈希算法生成签名。服务端收到请求后会用同样的规则重新计算签名并比对如果一致就放行。拦截器的作用是让注入逻辑与业务代码解耦。不管业务代码里调用client.get还是client.post拦截器都会在请求发出前自动执行给请求头加上这两个动态参数。这个模式在真实的自动化脚本里非常实用因为业务代码不需要感知到签名逻辑的存在。3.2 参数格式与签名计算方式详解上面代码里的generateRi函数默认返回22个字符的字符串前6位是时间戳末6位后8位是随机字符中间没有拼接符。如果你在看真实站点的代码时发现参数更长那可能是多拼接了一些固定前缀或者设备指纹信息。格式不需要和真实站点完全一样关键是理解这个字符串的每一段分别来自什么地方哪些是可变的哪些是固定的。签名计算这块我强烈建议你在断点里逐步观察真实的拼接顺序。有的实现会先对请求体做MD5摘要再把摘要拼进签名串有的实现会把URL里的查询参数做字典序排序后再拼接还有的实现会在拼接前对字符串做一次base64编码。这些细节差一点签名结果就完全不同。我在分析过程中总结出一个通用排查顺序先确认签名串里是否包含请求体再确认URL是完整路径还是去掉了域名然后确认是否包含时间戳、随机数、x-sap-Ri的值最后确认密钥串是固定写在代码里还是从某个配置接口动态获取。按这个顺序逐个验证基本能还原出完整的签名规则。3.3 用Node测试脚本验证参数在服务端的接受程度写一个test.js文件模拟一次真实的请求生命周期const client require(./client); async function main() { try { const res1 await client.get(/api/health); console.log(请求1状态:, res1.status, 返回体:, JSON.stringify(res1.data).slice(0, 100)); const res2 await client.get(/api/health); console.log(请求2状态:, res2.status, 返回体:, JSON.stringify(res2.data).slice(0, 100)); const res3 await client.post(/api/orders, { productId: test001 }); console.log(请求3状态:, res3.status, 返回体:, JSON.stringify(res3.data).slice(0, 100)); } catch (e) { console.error(请求失败:, e.message); if (e.response) { console.error(服务端响应:, e.response.status, JSON.stringify(e.response.data).slice(0, 200)); } } } main();跑这个脚本时分别观察连续两次GET请求之间x-sap-Ri的变化规律应该每次都不同以及POST请求的x-sap-Sec是否与GET请求有明显差异。正常情况下因为POST请求体参与签名计算x-sap-Sec的哈希值长度一样但内容完全不同。如果服务端返回403或者400先别急着改代码。用curl或者Postman手动发一次不带参数的请求确认是不是服务端本身就需要额外的Cookie或者Token。很多细节问题其实不是出在动态参数的生成上而是出在请求头缺了别的字段。4. 常见问题与排查技巧实录这部分我挑几个在复现和分析过程中出现过的高频问题按问题表现→排查思路→最终解决的格式整理成速查表。你之后遇到类似情况可以省不少时间。4.1 服务端返回403或401时的排查顺序问题表现本地脚本请求接口时返回403 Forbidden偶尔返回401 Unauthorized。排查思路第一优先检查是不是少了Cookie或者Authorization头。动态参数只是请求头的一部分如果服务端同时校验登录态缺失Cookie一样会403。第二检查请求头里除了动态参数外是否缺少User-Agent、Origin、Referer等浏览器会自动带上的头。服务端的风控策略经常组合校验这些字段。第三才是检查x-sap-Sec的生成规则。如果前面两项都没问题那大概率是签名串拼接顺序和真实规则不一致逐一对比调试即可。我在处理这类问题时会先在浏览器里手动发一次同样的请求把全部请求头导出成HAR格式和本地脚本发出的请求头做对比。这个对比能非常直观地发现缺了哪些字段、哪些字段值不一样。4.2 参数值规律正确但服务端依旧拒绝问题出在哪问题表现生成的x-sap-Ri每次都不同、长度也对x-sap-Sec的格式也是正确的64位十六进制但服务端仍然拒绝。排查思路这时候要怀疑的不是参数格式而是签名串的拼接规则。很多实现里签名串的一部分是请求体序列化后的字符串如果JSON的key顺序不同序列化结果就不同。服务端是把请求体里的key按字典序排序后再参与签名计算的而你本地直接JSON.stringify用的是原始顺序两边算出来的哈希值自然不一样。还有一个常见的坑是时间戳。有些实现的签名串里包含服务器时间而服务端会校验这个时间戳和自身时间的偏差如果偏差超过一分钟就拒绝。本机时间如果和服务器时间差太多即使签名规则完全正确也会被拒。4.3 x-sap-Ri和x-sap-Sec的生成规则会随环境变化吗这个问题后台经常有人问结论是对于同一站点同一时间段的生成规则基本稳定但服务端有灰度发布机制可能一部分用户用的是旧规则另一部分用户用的是新规则。所以你在某一天逆向出来的规则过了几周可能就失效了。我建议把逆向结论整理成一份带版本号的文档记录当时的站点版本、参数格式、生成算法和验证结果。一旦发现参数失效先查站点版本是否更新再对照文档里记录的规则逐个排查哪部分变了。这个习惯能帮你把维护成本降很多。4.4 分享一个提高分析效率的调试习惯最后分享一个小技巧在用Chrome开发者工具调试前先在浏览器设置里启用Disable cache然后打开开发者工具的Network面板勾选Preserve log。这样切换页面时请求记录不会被清空方便在页面跳转之后回看之前发出的请求上下文。另外在Sources面板里对压缩后的JS文件右键选择Pretty print格式化代码几乎能还原出60%到80%的原始可读性分析体验完全不一样。我在实际分析过程中还会开一个单独的Console面板把生成的参数值打印出来直接比对每次请求的参数变化。比如连续点击同一个按钮十次看x-sap-Ri的哪个片段在变、是否递增、是否包含固定的字符集分布规律。这些细节都是判断生成算法类型的重要依据。关于这个内容的后续我在自己项目里还遇到过类似的名字不同的动态参数但破解思路完全一样先锁定生成位置、再拆解生成规则、最后用脚本复现并用拦截器注入。这套方法论是通用的学会一次就能套用到大量类似场景。我在实际使用中最深的体会是动态参数的逆向功夫不是在代码审计上而是在抓包和对比上。把每次请求的参数变化规律形成肌肉记忆才是真正提升效率的关键。