做爬虫开发时最容易卡住的位置往往不是请求怎么发而是请求里的参数怎么来的。同一个地址在浏览器里刷新一次就能拿到数据换成 requests 或 curl 再请求服务端却直接返回异常。这种情况多数是因为页面里有一段 JavaScript 在运行过程中动态生成了附加参数请求时必须携带否则服务端无法通过校验。这类参数通常被称为动态参数、加密参数或签名参数。爬虫逆向 JS 的入门难点就是搞清楚这类动态参数是如何生成的。反过来说在一个 Web 项目里设计接口防篡改能力时也必须先理解同一套参数生成思路。这篇文章会从服务端校验的角度解释动态参数出现的原因并用一个自建的本地模拟接口完整走一遍“观察参数变化、定位生成函数、用 Node.js 复现逻辑、用 Python 验证请求”的流程帮助你把动态参数生成这件事真正弄明白。需要先说明边界文中的模拟接口完全自建不针对任何线上站点。如果将来要对外部站点做分析请先确认自己拥有正当调试权限并把目标限定在授权范围、公开接口或自有接口中。1. 动态参数到底是什么服务端为什么要校验它1.1 先区分固定参数和动态参数一次普通 HTTP 请求中真正参与业务逻辑的参数可以分为两类固定参数值相对稳定例如固定的分类 ID、固定的语言标志。业务参数由用户输入或页面状态决定例如查询城市、分页页码。动态参数每次请求都可能变化并且往往由 JavaScript 在前端运行时计算得到。爬虫开发中最容易卡住的就是第三类。动态参数有几个非常明显的特征特征说明示例时效性参数只在短时间内有效超过时间窗口后失效timestamp、expire随机性每次请求都生成新值避免固定值被直接复用nonce、uuid绑定性与请求路径、请求体、用户会话或环境信息绑定sign、token、deviceId例如一个带签名的请求常见形式是GET /api/weather?citybeijingtimestamp1735000000001nonceabc123signe5f6a7b8...其中timestamp是当前时间戳nonce是随机串sign是根据路径、参数和一段 secret 计算出的摘要。去掉其中任何一个服务端都会拒绝请求。1.2 服务端为什么要在意动态参数动态参数不是服务端刁难客户端它的核心目的是让请求“可验证、难伪造”。以最常见的签名机制为例完整链路可以拆成下面几步客户端收集请求路径和业务参数。客户端按约定顺序把这些值拼接成一个字符串。客户端对拼接串做摘要计算例如 MD5 或 SHA-256得到签名。客户端把签名和其他动态参数一起放进请求。服务端用同一套规则重新计算签名。服务端对比两个签名并检查时间差和随机数是否已被使用。这样一个接口服务的价值在于防篡改如果请求参数被中间人修改服务端重算签名后会发现不一致。防重放即使请求被原样抓包只要 timestamp 过期或者 nonce 已经用过服务端可以拒绝。提高批量请求门槛外部请求方必须完整复现 JavaScript 的逻辑才能生成合法请求。理解这一点很重要因为逆向 JS 的本质不是“背代码”而是重新走通这条链路。1.3 动态参数不只等于签名很多人把动态参数直接等同于 sign实际上并不准确。签名只是动态参数中最常见的一种。按生成方式和用途动态参数还可以分成这几种类型参数名常见形式生成方式服务端校验方式时间戳类ts、timestamp、_Date.now()或Math.floor(Date.now() / 1000)与服务器时间比较差值需在窗口内随机数类nonce、rand、uuid随机字符、时间戳加随机数记录已使用集合防止重放摘要签名类sign、signature拼接参数后做 MD5、SHA、HMAC 等计算使用相同拼接规则重算并对齐对称加密类data、params、payload把真实参数用 AES、DES 等算法加密服务端解密后再处理业务逻辑环境指纹类fp、deviceId、environment采集 UA、canvas、webgl 等特征后计算 hash绑定设备或会话一次数据多段请求共享Cookie 动态值Cookie 中某个字段登录后由服务端下发或由 JS 在请求前刷新服务端读取会话状态文章后续演示的是“时间戳 随机数 摘要签名”的组合这也是入门时最典型的一种组合。2. 理解动态参数前先准备一套可复现的调试环境动态参数逆向很容易变成“凭感觉看代码”。为了避免这个误区建议先在当前电脑上准备一套最小调试环境并把一个自建带签名接口跑起来。这样后面每一步分析都能在本地复现不会碰到权限、误操作或合规边界问题。2.1 工具清单工具主要用途使用阶段Chrome 或 Edge DevTools查看请求参数、调用栈、全局搜索 JS、断点调试定位参数生成逻辑Node.js直接运行和复现 JS 逻辑也是本地模拟服务端的基础环境逻辑复现Python 3使用 requests 发起业务请求验证签名是否正确最终验证Postman 或 Apifox快速重放接口逐个修改参数观察结果调试文本编辑器或 IDE阅读格式化后的 JS 代码阅读复杂逻辑环境检查可以直接执行node -v python3 --version curl --version只要三个命令都能正常输出版本本地模拟环境就基本可用了。2.2 在本地搭一个带签名参数的模拟接口为了把动态参数还原过程讲清楚这里写一个不依赖第三方依赖的 Node.js 服务。它只做两件事在根路径返回一个带有动态参数生成逻辑的 HTML 页面。在/api/weather接口校验时间戳、nonce 和 sign。先创建目录结构demo-dynamic-param/ ├── server.js ├── public/ │ └── index.html └── python_client.py服务端代码server.jsconst http require(http); const fs require(fs); const path require(path); const crypto require(crypto); const SALT demo-salt-2024; const TIME_WINDOW 180000; function sha256(text) { return crypto.createHash(sha256).update(text).digest(hex); } const pageHtml fs.readFileSync(path.join(__dirname, public, index.html), utf-8); const server http.createServer((req, res) { const url new URL(req.url, http://127.0.0.1:3000); if (url.pathname / req.method GET) { res.writeHead(200, { Content-Type: text/html; charsetutf-8 }); res.end(pageHtml); return; } if (url.pathname /api/weather req.method GET) { const city url.searchParams.get(city) || beijing; const timestamp Number(url.searchParams.get(timestamp)); const nonce url.searchParams.get(nonce); const sign url.searchParams.get(sign); if (!timestamp || !nonce || !sign) { res.writeHead(401, { Content-Type: application/json; charsetutf-8 }); res.end(JSON.stringify({ code: 401, msg: missing dynamic params })); return; } const now Date.now(); if (Math.abs(now - timestamp) TIME_WINDOW) { res.writeHead(401, { Content-Type: application/json; charsetutf-8 }); res.end(JSON.stringify({ code: 401, msg: timestamp expired })); return; } const expectSign sha256(/api/weather${city}${timestamp}${nonce}${SALT}).slice(0, 32); if (sign ! expectSign) { res.writeHead(401, { Content-Type: application/json; charsetutf-8 }); res.end(JSON.stringify({ code: 401, msg: invalid sign })); return; } const data { city, weather: sunny, temp: 28, note: demo data }; res.writeHead(200, { Content-Type: application/json; charsetutf-8 }); res.end(JSON.stringify(data)); return; } res.writeHead(404, { Content-Type: text/plain; charsetutf-8 }); res.end(not found); }); server.listen(3000, 127.0.0.1, () { console.log(demo server running at http://127.0.0.1:3000); });前端页面public/index.html内部会生成动态参数!DOCTYPE html html langzh-CN head meta charsetUTF-8 / title动态参数演示/title /head body h3天气查询带动态参数/h3 input idcity valuebeijing placeholder城市 / button idbtn查询/button pre idresult/pre script const SALT demo-salt-2024; async function sha256(text) { const data new TextEncoder().encode(text); const digest await crypto.subtle.digest(SHA-256, data); return Array.from(new Uint8Array(digest)) .map((b) b.toString(16).padStart(2, 0)) .join(); } function createNonce() { return Math.random().toString(36).slice(2) Date.now().toString(36); } document.getElementById(btn).addEventListener(click, async () { const city document.getElementById(city).value || beijing; const timestamp Date.now(); const nonce createNonce(); const path /api/weather; const sign await sha256(${path}${city}${timestamp}${nonce}${SALT}); const signPart sign.slice(0, 32); const resp await fetch(${path}?city${city}timestamp${timestamp}nonce${nonce}sign${signPart}); const json await resp.json(); document.getElementById(result).textContent JSON.stringify(json, null, 2); }); /script /body /html启动服务node server.js浏览器打开http://127.0.0.1:3000点击“查询”能看到服务端返回天气 JSON。这个例子中前端页面主动把签名规则写在了 HTML 里。真实线上场景里这段逻辑往往被打包进一个压缩后的.js文件并混入大量无关代码。但你需要理解的核心规则是一致的客户端拼接原始字符串计算摘要服务端再用相同规则比对。2.3 Network 面板要怎么配合使用在浏览器开发者工具里打开 Network 面板勾选 Preserve log然后点击页面上的“查询”按钮。你能看到一条/api/weather请求。打开这条请求在 Payload 或 Query String Parameters 里可以清楚看到四个参数city: beijing timestamp: 1735000000001 nonce: m8x2kf3xyz sign: e5f6a7b8c9...Network 面板中真正要关注的不只是参数值还有 Initiator 列。它表示这条请求是由哪个 JS 函数触发的。点击该列里对应的调用位置浏览器会直接跳到 Sources 面板中的相关代码行。这是定位动态参数生成逻辑最快的方式之一。2.4 验证动态参数确实“动态”多刷新几次页面每次都点击查询然后对比请求参数请求序号timestampnoncesign11735000000001m8x2kf3abce5f6a7b8c9d0...21735000000002n1f9aqw2def94a3b7c2e1f0...31735000000003p3l8qw1ghic21d9e8f7a6b...timestamp会随点击时间变化nonce使用了随机数sign因为 nonce 不同而不同。这就是动态参数第一次出现时的样子。如果这三个参数全部固定不变服务端就很容易被外部脚本直接复用也就起不到防篡改和防重放的作用。3. 用最小案例走通“定位参数、还原逻辑、验证请求”全流程上一节的模拟接口已经给出了完整源码。真实项目中很多参数生成代码不会直接摆在你面前。下面把分析过程拆成几个步骤即使面对压缩过的 JS这套流程也仍然成立。3.1 先用浏览器确认参数来源重新打开http://127.0.0.1:3000操作过程如下按 F12 打开 DevTools。切到 Sources 面板。在页面点击“查询”按钮。观察 Network 面板中/api/weather请求的 Initiator。点击 Initiator 里的调用栈会跳到public/index.html中fetch调用所在位置。继续向上找可以看到nonce createNonce()、timestamp Date.now()、sign await sha256(...)三行代码。这说明动态参数的来源不是服务端返回的而是在浏览器运行时由 JavaScript 计算出来的。逆向任务的本质就是找到这三行对应的逻辑并在 Node.js 或 Python 中重新实现。3.2 用全局搜索定位拼接字符串如果页面里没有源码只有一个压缩后的 JS 文件常见做法是使用全局搜索。快捷键Windows 和 LinuxCtrl Shift FmacOSCommand Option F优先搜索下面这些关键词sign nonce timestamp /api/weather Math.random Date.now搜索sign可能命中很多位置所以不要只搜这一个词。更推荐搜索接口路径或参数名组合例如/api/weather citytimestamp nonce也能直接搜索Math.random因为这往往是 nonce 或随机数参数的源头。3.3 用 XHR/fetch 断点暂停在发请求前更稳定的定位方式是给请求打断点。操作步骤在 DevTools Sources 面板右侧找到 XHR/fetch breakpoints。点击输入weather代表只在 URL 包含weather的异步请求触发时暂停。刷新页面并点击“查询”。代码会停在fetch(...)调用执行之前。在右侧 Call Stack 面板逐层向上点击观察每一帧中 Scope 里的变量。当你看到timestamp、nonce、sign三个变量同时出现在某个作用域中时那个作用域就是动态参数真正被构造的地方。这是比全局搜索更直观的方法尤其适合压缩混淆后的代码。3.4 用 Node.js 复现浏览器里的生成逻辑定位到生成逻辑后把它从浏览器环境里“搬”到 Node.js 中。只要这段逻辑没有依赖window、document、crypto.subtle等浏览器专用 API可以原样运行。基于模拟页面Node.js 版本的签名函数可以这样写const crypto require(crypto); const SALT demo-salt-2024; function createNonce() { return Math.random().toString(36).slice(2) Date.now().toString(36); } function buildSign(path, city, timestamp, nonce) { const raw ${path}${city}${timestamp}${nonce}${SALT}; return crypto.createHash(sha256).update(raw).digest(hex).slice(0, 32); } function buildQuery(city beijing) { const path /api/weather; const timestamp Date.now(); const nonce createNonce(); const sign buildSign(path, city, timestamp, nonce); return { city, timestamp, nonce, sign }; } console.log(buildQuery(beijing));保存为demo.js运行node demo.js输出示例{ city: beijing, timestamp: 1735000000004, nonce: m8x2kf3abc4f, sign: e5f6a7b8c9d0 }把这里的参数复制到浏览器地址栏http://127.0.0.1:3000/api/weather?citybeijingtimestamp1735000000004noncem8x2kf3abc4fsigne5f6a7b8c9d0能看到服务端正常返回数据说明 Node.js 复现成功。这里有一个容易被忽略的关键点观察代码里的timestamp是数字类型但在拼接字符串时它变成了字符串。JavaScript 的模板字符串会自动做类型转换。如果换到其他语言例如 Python就要主动确认参数是字符串还是数字否则拼接串会不一致。3.5 用 Python 请求完成一次闭环验证当签名逻辑用 Node.js 复现完成之后实际业务请求不一定需要使用 Node.js。你可以直接把这个签名规则翻译成 Python用 requests 发起请求import hashlib import math import random import time import requests SALT demo-salt-2024 def create_nonce(): # 复刻浏览器端 createNonce 的大致格式 rand_part format(random.random(), .15f).split(.)[1] # 转成 36 进制字符串模拟 JS toString(36) ts_part format(int(time.time()), x) return f{rand_part}{ts_part} def build_sign(path, city, timestamp, nonce): raw f{path}{city}{timestamp}{nonce}{SALT} return hashlib.sha256(raw.encode(utf-8)).hexdigest()[:32] def fetch_weather(citybeijing): path /api/weather timestamp int(time.time() * 1000) nonce create_nonce() sign build_sign(path, city, timestamp, nonce) params { city: city, timestamp: timestamp, nonce: nonce, sign: sign, } resp requests.get(http://127.0.0.1:3000/api/weather, paramsparams, timeout5) print(resp.status_code, resp.json()) if __name__ __main__: fetch_weather()运行python3 python_client.py预期输出200 {city: beijing, weather: sunny, temp: 28, note: demo data}到这里一条完整的“浏览器生成动态参数 - 定位 JS 逻辑 - 使用 Node.js 还原 - 使用 Python 请求”的链路已经打通。3.6 失败分支也要验证动态参数分析只验证成功路径是不够的。很值得故意构造几个错误场景确认服务端校验是否真的生效# 不带 sign curl http://127.0.0.1:3000/api/weather?citybeijingtimestamp1735000000004nonceabc # 时间戳过期 curl http://127.0.0.1:3000/api/weather?citybeijingtimestamp1000000000000nonceabcsignxxx # sign 拼写错误 curl http://127.0.0.1:3000/api/weather?citybeijingtimestamp1735000000004nonceabcsignwrong模拟服务会分别返回{code: 401, msg: missing dynamic params} {code: 401, msg: timestamp expired} {code: 401, msg: invalid sign}失败信息是调试动态参数时最重要的线索。服务端的返回提示越具体越容易缩小问题范围。4. 没有源码时怎样从外部请求定位动态参数上面的演示依赖接口源码但真实分析场景往往只有压缩混淆后的 JS 和一堆请求日志。下面几个方法不从“读代码”开始而从“观察行为”开始帮助你在不完整信息下定位动态参数。4.1 先对比多次请求找出变化字段拿到一个真实接口后不要急着搜索。先在浏览器里连续发起几次相同业务请求把请求头、查询参数和请求体完整复制下来放进表格对比。请求字段第一次请求第二次请求第三次请求是否动态citybeijingbeijingbeijing否page111否timestamp173500000000117350000000021735000000003是noncem8x2kf3abcn1f9aqw2defp3l8qw1ghi是signe5f6a7b8...94a3b7c2...c21d9e8f...是变化字段多数情况下就是动态参数。其中sign往往是摘要签名timestamp是时间戳nonce是随机数。接下来再逐步修改它们观察服务端返回差异。4.2 用 XHR/fetch 断点卡住请求发起位置把 Network 面板里看到的请求 URL 关键词记下来例如weather。然后在 Sources 面板里设置 XHR/fetch breakpoints展开 XHR/fetch breakpoints。点击加号输入weather。刷新页面。代码暂停在异步请求发起前。在 Call Stack 中向上查看函数调用链。在调用栈里重点关注那些包含“参数对象构造”的帧。DevTools 还会显示每个函数作用域里的局部变量比如timestamp、nonce、sign。如果发现某一帧的作用域里正好有这三个变量那这一帧就是参数生成的源头。4.3 不要只凭长度猜测算法很多新手看到 32 位十六进制字符串就认为是 MD5看到 64 位就认为是 SHA-256然后直接套用标准库计算。这个习惯很容易出错因为服务端可能先拼接参数再对结果做 MD5最后截取前 16 位。可能是 SHA-256 输出后再经过 Base64 编码。可能先做一次摘要再把摘要加入第二次拼接。可能拼接顺序并非肉眼所见而是经过排序后的参数名顺序。更稳妥的做法是先完整还原拼接串再逐个尝试常见摘要算法然后把结果与浏览器中真实出现的sign比对。比对成功后再确认算法细节。4.4 遇到压缩混淆后的处理思路压缩混淆会显著增加阅读成本但不会改变 JS 的运行机制。常见处理手段混淆类型表现应对思路变量名压缩变量名为a、b、c语义丢失不首先尝试全局重命名先借助断点观察运行时的值字符串编码参数名和 URL 被拆散或转义关注实际运行的请求结果而不是静态字符串控制流平坦化大量switch、while混在一起优先看调用栈跳过无关函数动态执行使用eval、Function构造代码检查调用栈中是否存在动态生成代码必要时在断点处打印值这个阶段最实用的三个技能是断点、作用域观察和变量值对比。静态阅读文字意义有限因为行得通的其他方法是用 Node.js 直接执行目标 JS再观察函数返回值。不过这一步需要更好的环境隔离适合作为进阶练习。5. 常见动态参数识别与处理对照表不同动态参数的定位方式差异很大。下面的表格可以作为速查参考动态参数类型常见参数名定位线索处理思路需要注意时间戳ts、timestamp、_数值接近当前毫秒或秒使用int(time.time() * 1000)或Date.now()生成确认是秒还是毫秒保持类型一致随机数nonce、rand、uuid每次变化无规律请求前重新生成每次请求使用新值不要重复使用同一个 nonce避免重放风险摘要签名sign、signature固定长度 hex通常 16/32/64 位还原拼接顺序和摘要算法补齐截断方式拼接串中参数顺序错误是最常见失败原因对称加密data、params、payload参数值是一长串密文可读性差还原加密算法、模式和密钥补齐 IV密钥可能随响应下发也可能固定在 JS 中环境指纹fp、deviceId参数跨多次请求保持不变观察它是否和 UA、Canvas、WebGL 特征绑定如果指纹来自浏览器环境本机复现需构造一致环境Cookie 动态值Cookie 中一段字段请求前由 JS 写入 Cookie检查 Cookie 是登录下发还是 JS 计算如果是登录会话直接使用登录后的请求流程即可响应式 Tokentoken、access_token登录后返回过期后刷新使用官方认证流程获取维护刷新机制不要尝试伪造认证信息这超出逆向学习范围这个表中的处理方式只解决“动态参数怎么生成”的问题。如果某个参数本身是认证凭证的一部分请使用正常的登录和授权流程而不是绕过认证。6. 从入门到实践的常见坑与排查路径动态参数开发调试中大多数问题集中在拼接规则、时间格式、环境差异和类型转换上。下面列出最常出现的五种问题并给出从现象到原因的排查顺序。6.1 时间戳导致签名失效现象本地生成的签名第一次请求成功第二次就失败或者服务端返回时间过期。常见原因使用秒级时间戳但服务端按毫秒计算。当前机器时间与服务器时间相差过大。生成签名之后又等待了很久才发出请求默认时间窗口只有 180 秒。检查方式python3 -c import time; print(int(time.time() * 1000), int(time.time()))对比接口返回的时间戳单位和服务器返回的错误信息确认是毫秒还是秒。6.2 拼接串顺序不一致现象返回invalid sign但时间戳和 nonce 都正确。检查方式把浏览器实际发出的sign保存下来。打印自己拼接的原始字符串。尝试调整参数顺序例如path city timestamp nonce和city path nonce timestamp。对比最终签名。推荐写法把原始拼接串打印到日志中并保留一个独立的测试用例。6.3 本地复现和浏览器结果不一致常见原因字符编码不一致例如浏览器使用 UTF-8Python 请求默认编码不匹配。拼接串包含空格、换行或隐式类型转换。服务端实际使用的路径是完整 URL而不是路径部分。搜索到的 salt 或密钥来源错误。检查方式在浏览器中临时改写 JS把拼接串输出到页面或 Console再与本地产出生成的字符串逐字符比对。6.4 参数依赖浏览器环境现象同样的签名算法在 Node.js 中生成的值与浏览器中不一致。常见原因原代码调用了window、document、navigator、canvas或crypto.subtle等环境相关 API。处理思路优先只抽出与签名计算相关的纯函数。用 Node.js 内置crypto模块替代crypto.subtle。对navigator.userAgent等环境值手动补一个固定字符串。不要试图把整段业务 JS 原样搬进 Node.js 运行避免无关代码干扰。6.5 推荐排查顺序当你拿到一个“签名失败”的动态参数问题时按下面清单逐项排查浏览器实际请求中的参数是否齐全有没有额外参数写在请求头或请求体中。接口路径是 path 还是完整 URL是否包含 query。拼接顺序是否与页面代码完全一致。摘要函数选择是否匹配是否需要截断。输入字符串编码是否为 UTF-8。签名时使用的 timestamp 和 nonce 是否与最终发送的值一致。时间单位是秒还是毫秒。是否还有额外请求头、Cookie、IP 信息参与签名。每次修改后只调整一个变量不要同时修改多个参数否则很难定位问题。7. 动态参数逆向真正值得投入的时间应该花在哪动态参数逆向入门之后很容易让人陷入一个误区觉得所有接口都要逆向所有逆向都必须靠硬啃 JS。实际工程里最高效的顺序是先判断是否需要逆向再选择成本最低的方式。7.1 先考虑不需要逆向的方案部分数据获取方式比逆向 JS 更稳定、更合规场景更合适的方案需要天气、汇率、股票等公开数据优先使用官方开放接口页面本身包含 JSON 数据直接解析页面返回内容公司内部接口缺少自动化测试让后端给出测试接口或动态 token 方案低频次、小批量数据手动导出或使用官方报表高频次、大批量商业数据走商务合作或官方 API不要依赖逆向对抗如果需求里已经出现验证码识别、绕过登录、突破访问控制等字眼说明场景不适合公开技术讨论也不适合作为学习练习。正确的选择是停下改用授权和合作方式。7.2 动态参数学习更适合用在哪些正当场景自建前端页面做反篡改设计判断现有签名是否足够安全。对自有接口做自动化回归测试时让测试脚本生成合法签名。在一次安全测试中验证某个接口是否存在参数篡改风险时需要理解签名的生成过程。学习 HTTP、加密算法和前端调试把动态参数作为串联知识点的实验项目。这些场景的共同前提是测试对象是你自己的系统或者你已获得明确授权。7.3 工程化封装签名模块一旦签名逻辑在测试环境中跑通不要直接把它写进爬虫脚本里草草了事。推荐把它封装成独立模块sign-client/ ├── index.js ├── signer.js ├── test/ │ ├── signer.test.js └── README.mdsigner.js里只负责生成签名不负责发送请求。这样设计的好处是签名逻辑可以被 Node.js 和浏览器复用。单元测试可以覆盖不同参数、不同时间戳、不同 nonce 的签名结果。服务端签名规则变更时只需要改一个模块。一个简化的具备单测结构的签名模块示例const crypto require(crypto); const SALT process.env.DEMO_SALT || demo-salt-2024; function sha256Hex(text) { return crypto.createHash(sha256).update(text, utf-8).digest(hex); } function createNonce() { return Math.random().toString(36).slice(2) Date.now().toString(36); } function buildSign(path, city, timestamp, nonce) { const raw ${path}${city}${timestamp}${nonce}${SALT}; return sha256Hex(raw).slice(0, 32); } function buildQuery(city beijing) { const timestamp Date.now(); const nonce createNonce(); return { city, timestamp, nonce, sign: buildSign(/api/weather, city, timestamp, nonce), }; } module.exports { createNonce, buildSign, buildQuery };配合简单的单元测试const assert require(assert); const { buildSign } require(../signer); const sign buildSign(/api/weather, beijing, 1735000000004,