资讯动态

动态验签与JS逆向实战:从接口签名到数据采集全流程解析

发布时间:2026/10/17 9:38:25 来源:尧图企业网站定制
1. 项目来由与自动化目标为什么盯上这块“硬骨头”最近手头一个数据分析项目需要拉取某水利建设市场监管平台上的公开项目信息用来做施工企业中标活跃度统计。这类平台页面乍一看不算复杂但它把所有查询接口都套了一层动态验签同一个接口参数稍有变化、时间戳换了sign字段都会变拿不到签名直接构造 HTTP 请求多半会被拒。这篇文章算是一次完整的实战复盘从定位加密入口、梳理验签逻辑到把采集程序跑稳定再把几个有代表性的坑放在一起说。如果你正在处理类似公开数据采集的项目或者刚开始接触 JS 逆向想看看真实业务场景里怎么用这篇应该能给你一个比较完整的参考。但有必要先说清楚边界本文只聊公开可浏览数据的接口理解不涉及用户账号体系、不尝试绕过任何认证和验证码更不做高频抓取。技术分析归技术分析合规使用永远是前提。1.1 数据普查场景与手工流程的困境当时的需求并不复杂想统计某区域内水利施工企业近三年的中标项目数量、中标金额分布以及不同资质等级企业的活跃情况。这类数据在两个地方能找到一是公开新闻通稿二是平台的项目信息公示页。后者字段更规整有项目编号、项目名称、中标单位、中标金额、发布日期基本就是我们需要的结构化数据。但真正拿到手之后才发现平台列表页是后端渲染加前端拼接一次性只显示固定条数。要采完一个季度的数据大概要翻上百页。手工操作无非是复制表格、翻页、再复制几个人的小组分工也得忙一个星期而且人工复制最容易出错的地方恰恰是金额和日期表格一换行、数字一多眼睛根本盯不过来。于是自动化采集自然被提上日程需求也从“写一个脚本”变成了“把这套查询机制吃透”。1.2 绕过页面直接请求接口的动机能选的路其实有两条方案 A用浏览器自动化工具模拟点击和滚动把渲染后的页面抓下来方案 B直接在 Network 面板里找数据接口拿到结构化 JSON自己写程序请求。方案 A 在开发初期特别快因为页面长什么样浏览器就抓什么不用关心前端逻辑。但它的缺点也很明显要启动完整浏览器进程内存开销大页面异步懒加载和滚动时机不好控制一旦布局改版整套选择器可能作废。尤其这种带“动态验签”的平台浏览器里每次点查询都会重新生成签名浏览器自动化其实是把签名逻辑当成黑盒跳过去了后续要维护反而更麻烦。方案 B 看上去复杂一截但数据直接从接口拿字段干净、响应时间短翻页也只需要修改pageNo。问题在于这类平台不会让接口裸奔。它给查询接口加了一层动态验签除非你先把签名生成逻辑搞清楚否则后续稳定的采集无从谈起。这也是我把这个项目复盘出来的主要原因。1.3 动态验签和固定Token到底差在哪先放一个简单的对照表方便判断自己遇到的量级属于哪一种。对比项固定 Token动态验签有效期几小时到几天通常以分钟甚至秒为单位参数变化影响不影响已有 Token参数一旦变化签名需要重算防篡改能力弱较强服务端校验逻辑查缓存、查过期重新计算签名并逐项比对对采集方的影响拿一次 Token 就能反复用每次请求都要现场生成签名固定 Token 更像一把长期有效的钥匙只要不过期就能开门动态验签则是一把由“当前时间 随机数 参数内容”组合出来的临时钥匙每道门钥匙都不一样。前者在接口设计里实现简单但一旦 Token 泄露就比较难收拾后者校验成本略高不过能顺带挡住很多无脑请求。对这个项目来说第二道门并不是什么高不可攀的加密算法而是要求你必须把前端的一次请求完整还原出来。接下来要做的就是从浏览器里把这条链路找出来。2. 从浏览器到接口定位签名参数的完整路径很多人拿到这类平台第一反应是打开开发者工具然后盯着 Network 面板发愣请求是看到了但就是不知道哪个字段是签名签名又是从哪来的。我的做法是三步走先看请求特征再看调用堆栈最后用断点确认。2.1 打开 Network 面板后的第一条线索在页面搜索框里随便输入一个关键字点击查询盯着新增的 XHR 请求。正常的网络请求通常长这样请求地址/api/project/page或/query/projectList之类的列表接口参数pageNo、pageSize、keyword等业务字段附加字段timestamp、nonce、sign也可能叫_signature、signature。这几个附加字段就是突破口。timestamp一眼能认出来是毫秒级时间戳nonce是随机串通常用Math.random()生成sign则是核心签名。我最开始做的事是把一个页面的请求整体复制成cURL然后在本地用 Python 模拟先看看服务器到底拒绝什么。结果非常典型去掉sign返回“验签失败”timestamp改成过期值同样返回“验签失败”。这一步确认了平台确实在接口层做了动态验签而不是简单靠登录态或者 IP 限制。之后的问题就变成sign到底怎么算出来的需要从哪里复制逻辑。这里有个经验不要一上来就试图看懂整个 JS 文件先找“最近的变量来源”——也就是请求发出前那一刻参数对象里到底有哪些字段。2.2 用 Call Stack 完成现场还原网络请求面板里每个请求都有 Initiator 信息点进去能看到发起这个请求的调用栈。对于 XHR 来说栈里通常有send、open、XMLHttpRequest这些原生函数再往上就是页面的业务代码。如果框架是 Axios还会看到dispatchRequest、request这类封装函数。我的习惯是在 Network 面板右键选中的请求复制Object或cURL先留底点击 Initiator 里的文件链接跳转到具体 JS 文件在疑似发起请求的那一行前打断点重新触发一次查询看断点能不能命中。命中之后左侧作用域里能看到本次请求的所有参数对象包括真正传给后端的变量。顺着这个函数继续往上翻就能找到sign的赋值语句。这一步其实没有太高技术含量更多是耐心和细心。只要断点位置选得准请求参数的来龙去脉基本就暴露在眼前了。2.3 关键字搜索与断点调试的取舍有些平台把请求封装得比较好sign的赋值语句就在某个函数里搜sign就能搜到但更多时候源码是压缩混淆过的函数名可能是_0x1234直接搜sign会出来一大片根本分不清到底哪个是签名函数。遇到这种局面我建议优先走断点路线不要在源码里大海捞针。因为压缩后的代码可读性本来就差你要找的是“数据流”不是“某个具体的字符串”。断点能让你看到运行时真实值这比读一万行混淆代码都直观。记住一点页面是跑在浏览器里的只要能让它停下来就能看清它当时手里拿到了什么参数。如果页面里有多重混淆或者无限debugger陷阱也不用慌。无限debugger只是让调试器反复暂停可以在调试器设置里选择不在此处暂停或者用条件断点精确命中目标行。核心原则还是那句话盯着运行时数据别被无关代码带着走。2.4 请求封装层的简化模型为了表达得更清楚我用一段简化示例还原这类页面的常见封装它大概率不是平台原始代码但结构上很接近// 页面内部请求封装示例用 function loadProjectList(pageNo) { const params { pageNo: pageNo, pageSize: 20, keyword: document.getElementById(keyword).value.trim() }; params.timestamp Date.now(); params.nonce Math.random().toString(36).slice(2); params.sign computeSign(params); return axios.get(/api/project/page, { params, headers: { x-source: web } }); }从这段代码能直观看到业务参数先被收集进一个对象然后追加timestamp、nonce最后调用computeSign计算签名。我实际排查中做的事就是把computeSign这个函数从压缩后的代码里找出来再看它内部用了什么拼接规则、什么摘要算法、有没有固定盐值。把这几个点问清楚离真正复现签名就不远了。3. 动态验签机制拆解签名不是“加个密”那么轻巧这一节我想认真解释一下动态验签的逻辑因为很多朋友一看到“JS 逆向”就以为是去破解某个高深算法其实真正困难的地方往往在于理解“为什么这样设计”以及“参数在哪个环节参与签名”。3.1 服务端验签的完整闭环服务端收到请求后验签流程通常如下取出请求参数里的timestamp判断是否在允许的时间窗口内超时直接拒绝取出nonce检查是否已经被使用过防止同一签名重复提交按约定规则重新拼装参数排序、编码、拼接盐值计算出签名把计算出的签名和请求头里的sign做比对不一致就返回验签失败。这个闭环里的关键点在于服务器并不需要知道前端代码是怎么写的它只需要用同一套规则把接收到的参数重新算一遍。所以逆向工作的本质是还原服务器的计算规则而不是真的“破解”某个密钥。时间窗口这个设计尤其值得注意。它意味着签名不能无限期复用你上午抓到的完整合法请求下午原封不动再发一次很可能直接返回失败。很多刚上手的朋友在这里栽跟头总觉得自己代码没问题其实是服务端那套时间校验没通过。3.2 三种常见动态签名结构对比我在不同类型平台上遇到过三种比较常见的动态签名设计它们难度和特征都有区别类型典型特征逆向难度维护成本固定盐值 参数摘要sign是 32 位 hex规则为排序拼接后做 MD5/SHA1较低低但盐值一旦泄露就得全量换时间戳 随机数摘要请求里有明显的timestamp、nonce签名结果每次都不同中等中容易理解也容易改动态密钥协商需要通过一个接口先取 seed/sessionKey再参与摘要计算较高高服务端要维护会话状态这个项目遇到的属于中间档次时间戳、随机数、业务参数全部参与签名外面再套一层摘要算法。从防御角度看不算最强但已经能挡住绝大多数“拿到接口就裸调”的行为。对采集程序来说最难的不是看懂摘要算法而是把前端“动态获取 seed、更新盐值”的细节完整模拟出来。3.3 从特征到断点识别加密函数识别签名函数的思路可以从几个特征入手签名值长度32 位是 MD540 位是 SHA164 位可能是 SHA256 或自定义拼接参与计算的辅助字段salt、seed、sessionKey、timestamp、nonce敏感词sign、signature、encrypt、digest、hmac。找到候选函数后不要急着抄代码先在控制台手动调用一次。如果你能把候选函数挂到全局变量上比如window.getSign computeSign那就可以在 Console 里任意构造参数测试观察输出是否符合预期。这种方式比改源码加日志更省事也更容易定位到底哪一步出了问题。这里说句实话真正难的往往不是最后一个 MD5而是前面“参数的拼接顺序、大小写转换、编码方式”这些细节。任何一个环节和前端不一致签名结果就完全不同而且报错信息不会告诉你差在哪里。3.4 为什么要用动态验签而不是固定密钥有朋友问过既然平台也没那么高安全要求直接用固定签名不就行了为什么非要做动态验签这个问题要从防滥用角度理解。固定密钥一旦被采集方提取就相当于把门钥匙共享了服务端很难区分正常用户和自动化脚本。动态验签把“时间窗口 随机数 参数绑定”结合起来至少能做到参数一旦被篡改签名校验立刻失败一次请求的签名无法重复利用降低重放风险前端签名逻辑混淆后提取门槛被抬高。当然动态验签并不等于绝对安全它只是提高门槛。我写这些技术分析目的也是希望做数据采集的人理解这套机制而不是教大家去绕过平台的授权边界。理解机制之后你会更清楚哪些请求可以做、哪些不该做。4. 采集主流程落地参数构造、签名封装与限速策略机制梳理清楚之后剩下的就是工程化落地。这个环节容易让人误以为很难其实框架搭好之后代码量并不大难点集中在“如何保证每一次请求都生成合法签名”和“如何在线上稳定运行”。4.1 一次请求的完整生命周期我习惯把一次请求拆成清晰的步骤每一步都不要有隐性依赖初始化会话创建requests.Session()带上浏览器常见请求头获取基础配置访问首页或静态配置接口拿到需要的前端版本号、盐值、加密种子构造业务参数设置pageNo、pageSize、筛选条件等追加动态字段填入毫秒时间戳和随机nonce计算签名排序参数、拼接、摘要、转大写发起请求带上Referer、User-Agent、X-Requested-With等头解析响应把 JSON 或 HTML 里的数据转换成结构化表格翻页重试检查总页数按顺序继续遇到失败先限速再重试。这套流程里最关键的是第 5 步和第 6 步。很多验签失败不是因为签名算法谜之复杂而是因为少带了一个请求头或者排序规则跟前端不一致。4.2 签名逻辑的工程化封装下面给一个简化的 Python 封装示例它表达的是思路不是平台真实代码。真实项目里要把盐值、密钥、摘要算法从配置读取而不是硬编码在业务代码里import hashlib import random import string import time import requests def _nonce(length8): return .join(random.choices(string.ascii_letters string.digits, klength)) def build_sign(params: dict, salt: str) - str: payload dict(params) payload[timestamp] int(time.time() * 1000) payload[nonce] _nonce() raw .join(f{k}{payload[k]} for k in sorted(payload.keys())) raw fsalt{salt} return hashlib.md5(raw.encode(utf-8)).hexdigest().upper() def fetch_page(page_no: int): session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0, Referer: https://example-internal-page/, X-Requested-With: XMLHttpRequest, }) salt get_salt_from_static_config() # 从静态JS或配置接口读取 params { pageNo: page_no, pageSize: 20, keyword: , } params[sign] build_sign(params, salt) resp session.get(https://example-internal-page/api/project/page, paramsparams, timeout10) return resp.json()这段代码有几个细节值得强调签名里的params是在追加了timestamp和nonce之后才排序拼接的顺序不能乱sorted(payload.keys())得到的顺序要和前端一致前端多数是用Object.keys().sort()md5的输入是原始 UTF-8 字节不能先编码成str再编码容易出错返回签名转大写是因为页面里看到的就是大写。4.3 直接 HTTP 请求与浏览器自动化如何取舍开发阶段我强烈推荐先用浏览器自动化工具做验证因为可以看到完整页面上下文定位问题更快。但上线正式采集的时候我会优先选择直接构造 HTTP 请求。原因不难理解一个是重机枪一个是手术刀。浏览器自动化要启动完整内核处理页面渲染和网络等待直接 HTTP 请求只需要维护接口层速度快、资源占用低、可控性强。而且真正的动态验签逻辑一旦通过断点还原清楚浏览器自动化反而成了多余的一层。当然直接 HTTP 请求对工程质量要求更高。你要自己管超时、重试、限速、编码、会话保持还要应对接口返回的异常格式。如果你只是想临时拉几百条数据那用浏览器自动化脚本反而更划算不要为了“技术正确”把事情做复杂。4.4 限速、重试与失败兜底采集平台数据最忌讳“快”尤其这种带动态验签的接口频繁请求很容易触发风控。我通常会把请求间隔控制在 2 到 5 秒并且加一点随机抖动让请求节奏更接近人工操作。失败兜底方面我的策略是单次请求超时设为 10 秒遇到网络错误或状态码异常先等待 20 秒再重试最多试 3 次反复失败就写入日志并跳过当前页不中断整个任务每次跑完数据后把已抓取分页号记录到本地下次断点续采。这种设计能保证程序在无人值守情况下也能自行恢复。很多自动化脚本不是死在签名上而是死在不重试、不限速、不做断点恢复。细节做到位稳定运行一周基本没问题。5. 实际翻车现场复盘时间戳偏差、改版失效与编码陷阱这部分我从真实踩坑里挑了几个最具代表性的问题它们看起来都是小细节但每一个都让我浪费过大半天。5.1 时间戳“过期”与服务端时间漂移第一次把签名流程写成 Python 后我在本地测试一切正常但部署到服务器上就跑不通整天返回“验签失败”。排查到最后发现是两台机器的系统时间相差了几分钟。服务端校验时间戳时不会要求毫秒不差通常会给一个容差窗口比如前后五分钟。本地电脑时间没问题服务器时间落后了将近十分钟直接超窗。解决方案也很简单在程序启动时访问平台首页从响应头里的Date字段拿服务器时间或者调用一个公开的时间接口计算出本地与服务器的时钟偏移量后续所有timestamp都用服务器时间生成。这个坑也启发我调试阶段不要只盯代码逻辑服务器环境、系统时钟这些“外部变量”同样会影响签名结果。遇到诡异问题先把时间对齐再查其他。5.2 前端改版后签名入口“消失”还有一次是平台前端做了一次例行升级压缩后的 JS 文件名从app.abc123.js变成了app.def456.js而且原来的签名函数被移动到另一个模块里。我用来提取盐值的正则突然失效程序直接跑不起来。这个问题没办法靠“一次性逆向”彻底解决。我的处理方式是把“定位签名逻辑”与“实际生成签名”解耦在代码里维护一个明确的接口契约把签名函数、盐值来源、请求地址都抽成配置项。前端改版后先跑一次冒烟测试如果签名失败就重新定位一下入口只改配置或签名模块不影响整个采集主流程。没有人能保证外部平台永远不变所以工程上一定要给自己留出快速修复的口子。尤其要注意压缩后的 JS 文件名每次构建都可能变不要硬编码文件名最好通过解析首页 HTML 里的script标签动态获取。5.3 大小写、排序和 URL 编码的暗坑签名拼接规则的坑尤其隐蔽。比如参数排序是按字符串的 Unicode 码点排不是按拼音更不是按添加顺序后台计算签名时可能先把所有参数值做了encodeURIComponent也可能是先排序再编码哈希结果是十六进制小写但页面展示和请求里是大写必须转换一致含中文的参数不能直接拼接需要先统一编码否则服务端按另一个编码解码就对不上。我遇到最典型的情况是所有英文字段都能正常跑通一加中文筛选条件就验签失败。后来发现前端代码里先调用了decodeURIComponent而后端计算签名时却使用了encodeURIComponent之后的原始值。这个细节如果不在断点里看真实值光靠猜根本猜不中。建议在还原签名规则时把前端函数里每一步拼接结果都在控制台打印出来和本地实现做逐字符对比。哪怕差一个空格都是验签失败。5.4 数据解析中的字符实体问题签名问题解决之后我以为就万事大吉了结果数据又出了幺蛾子列表接口返回的 JSON 里企业名称和项目名称全是\u67d0\u67d0\u9879\u76ee这类 Unicode 实体打印出来没法直接入库。原因很简单平台在前端做过字符转义。处理方式也不复杂Python 里用json.loads或html.unescape转换一次就好。但容易忽略的是转义后的字符可能包含不可见字符入库前最好统一做一次去空格、去全角空格处理。这类问题属于“看起来不是技术难点但非常恶心”的类型。我的建议是把数据清洗步骤放到解析层不要散落在业务代码里这样至少出了问题能快速定位。6. 采集边界与工程化取舍合规、稳定和可维护性文章最后我想把边界和工程化取舍讲清楚。技术能力是双刃剑能不能用、怎么用比怎么写代码更重要。6.1 什么是公开数据什么是越界这次项目里我给自己定的原则是只采集无需登录、页面可直接查看的公开信息不尝试绕过验证码、不模拟他人账号登录、不抓取登录后才可见的数据如果页面或平台使用条款明确禁止批量抓取立即停止并考虑申请数据共享或采用官方服务采集频率保持在低水平不做全量并发请求不以压测方式打满接口。很多人在“公开数据”和“数据抓取”之间打擦边球我认为没必要。数据源是监管平台也好商业网站也好公开可见不等于可以任意无限量采集。真正需要长期运行的数据任务一定要在规则允许的范围内进行。6.2 用“最小必要”原则约束采集代码工程上我还会坚持“最小必要”原则只抓业务需要的字段不把整张表全量拖回本地做增量更新记录上次抓取到的截止日期下次只补增量本地做去重和索引避免重复请求留好日志和审计每一条数据都能追溯到采集时间和来源页面。这个原则既是合规需要也是降低维护成本的需要。字段越少解析越简单增量越小请求越少被风控盯上的概率也越低。数据任务不是“越多越好”而是“够用就好”。6.3 更省力的替代思路官方接口优先最后想给同行一个建议如果数据需求长期存在不要只想着做采集也要花时间找找官方通道。现在不少监管平台会提供数据开放接口、定期数据快照或者支持以单位名义申请数据共享。这些通道申请起来可能有点流程成本但一旦开通稳定性和合法性都远超临时逆向。即便申请不下来也可以考虑把人工导出和自动化结合起来用脚本处理表格而不是和接口验签死磕。JS 逆向是一项很好的技术能力但它更适合用来理解系统设计和解决临时问题。在长期运营的数据项目里稳定、合规、可维护才是第一位的。把签名逻辑封装好、边界划清楚哪怕平台明天改版你也能从容应对。我个人的体会是做这类外部数据采集最有价值的往往不是最后的代码而是中间逼着你读懂别人设计思路的过程。动态验签真正考验的不是加密算法有多难而是你愿不愿意把时间花在观察请求、验证假设、处理异常上。这次项目跑稳之后我把签名逻辑单独抽了出来后续改版只调配置不伤主流程。如果你也在处理类似公开数据采集建议先把边界划清楚再动手先合规再谈效率。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑