前端安全里有一类练手场景特别经典拿一个公开的翻译类网页做逆向分析把它的请求签名逻辑还原出来。我最早接触这类分析就是从翻译站点的接口开始的因为它请求结构清晰、参数不多、返回是明文 JSON非常适合用来建立抓包—定位—还原—验证的完整方法论。这篇就把我处理某翻译站点网页接口的完整过程摊开讲从打开开发者工具那一刻起到最终用本地脚本复现出可用的请求中间每一步的判断依据、走过的弯路、以及那些文档里不会写的细节都尽量还原出来。需要先说明的是本文讨论的是前端请求签名机制的学习与研究目的是理解常见的前端加密套路、提升自己系统的防护意识。所有分析都基于公开可访问的网页行为不涉及任何绕过服务条款的批量调用实际使用时请遵守目标站点的使用规则控制请求频率仅作技术学习。适合的读者是已经会用浏览器开发者工具、懂一点 JavaScript、想系统学习前端逆向分析流程的同学如果你连断点调试都没用过建议先把 Sources 面板的基本操作摸熟再往下看。关键词先摆出来方便你建立索引请求签名、参数加密、断点调试、调用栈回溯、混淆代码还原、本地复现验证。这几个词基本就是整条分析链路的主干后面每个章节都会围绕它们展开。1. 先搞清楚要还原的到底是什么东西很多人一上来就打开开发者工具乱点抓到一个请求看到一堆参数就开始懵。我的习惯是先做一件事把目标请求的输入和输出关系理清楚也就是搞清楚我给它什么它返回什么中间哪些字段是我看不懂的。这一步做扎实了后面定位代码会快很多。1.1 抓包定位目标接口打开翻译页面输入一段文字触发翻译在 Network 面板里筛选 XHR/Fetch 请求。翻译类站点通常会有一个明显的接口返回体是 JSON里面带着译文。找到它之后重点看两样东西请求方法和请求参数。以我处理的这个场景为例请求是一个 POST参数里除了明文的查询内容、语言方向之外还夹着几个看起来毫无规律的字段——一长串十六进制似的字符串以及一个时间戳相关的数字。这几个字段就是我们要还原的核心。我的判断逻辑很简单凡是看起来不像人手动填的、每次请求都变、且长度固定的字段大概率就是签名或加密参数。这里有个容易忽略的点不要只看一次请求。连续触发几次翻译对比同一个字段的变化规律。如果某个字段每次都不同但长度一致基本可以确定它是根据请求内容动态生成的签名如果它跟时间戳强相关那多半掺了时间因子。这个对比动作花不了两分钟但能帮你提前排除掉一半的干扰字段。1.2 建立参数—来源对照表定位到目标接口后我会在纸上或者笔记里列一张表把每个可疑参数和它的特征记下来。这张表在后面回溯调用栈的时候会反复用到因为你要不断问自己这个值是在哪一步被塞进请求里的。参数名代称特征初步判断是否每次变化query明文查询内容用户输入随输入变from/to语言方向固定枚举否sign32 位十六进制串疑似 MD5 类摘要是ts13 位数字时间戳是token较长混合串疑似多字段拼接后加密是这张表的价值在于它把未知变成了待验证的假设。比如我假设 sign 是某种摘要算法那验证方法就是——找到生成它的代码看它到底对什么内容做了运算。假设不成立就修正这个迭代过程就是逆向分析的本质。1.3 明确最小还原目标这里我要强调一个心态问题。新手容易陷入我要把整个 JS 文件每一行都看懂的执念结果三天过去还在读混淆代码。正确的做法是只还原到能复现请求为止。也就是说我不需要理解整个前端框架怎么运转我只需要找到那个把明文参数变成 sign 的函数把它单独拎出来喂同样的输入能得到同样的输出任务就完成了。这个最小目标的界定直接决定了你后面搜索代码的范围。带着找签名函数这个明确目的去搜比漫无目的地读代码效率高一个数量级。我见过太多人卡在第一步就是因为目标太宏大。2. 在混淆代码里把签名函数揪出来定位到接口之后真正的硬仗开始了目标站点的 JS 基本都是打包压缩过的变量名全是 a、b、c函数嵌套得跟俄罗斯套娃一样。这一章讲的就是怎么在这种环境里精准定位到关键函数而不是靠运气。2.1 用全局搜索做第一轮缩小最直接的手段是全局搜索CtrlShiftF。搜什么搜参数名。比如那个 sign 字段直接在 Sources 面板全局搜sign往往能搜到类似sign: xxx(...)或者params.sign ...的位置。这一步能快速把范围从几万行代码缩小到几个文件。但这里有个坑参数名可能被混淆或者拼接。我遇到过 sign 这个键名是通过变量拼出来的直接搜搜不到。这时候换个思路搜请求路径的一部分或者搜那个时间戳字段名往往能顺藤摸瓜找到组装请求参数的那段代码。搜索的关键是找一个相对独特、不容易被混淆掉的字符串接口路径、固定字段名都是好选择。搜到之后不要急着下结论先看上下文。如果搜到的位置只是把 sign 塞进对象那真正的生成逻辑还在上游需要继续往上追。2.2 断点打在值被写入的那一刻全局搜索只能给你一个大致方位真正精准的定位靠断点。我的习惯是在搜到的赋值语句那一行下断点然后触发一次翻译让代码停在那里。停下来之后看右侧的 Scope 面板把当前作用域里的变量都展开看一眼——那个 sign 的值是不是已经算好了如果是说明生成逻辑在更早的地方如果还是 undefined说明马上就要执行生成。更高效的做法是在 XHR/fetch 的发送处下断点。开发者工具支持在发起网络请求时断住XHR breakpoints 或者直接在 send 方法上下断这样无论签名逻辑藏得多深请求发出前一定会经过这里。停住之后往上翻调用栈Call Stack一层层看每一帧的局部变量通常两三帧之内就能看到那个把明文变成密文的函数调用。调用栈回溯是我认为整个逆向过程中最核心的技能。它的逻辑是请求发送是终点沿着调用链往回走就能找到起点。每一帧都问自己这一帧的入参是什么、出参是什么、它调用了谁答案自然浮现。2.3 读懂混淆后的函数结构定位到函数之后面对的是这样的代码变量名全是单字母逻辑被拆成无数个小函数还夹杂着大量的字符串数组和位移运算。别慌混淆代码有它的规律。第一字符串通常被集中到一个大数组里通过索引去取。看到_0x1234[0x1f]这种就去数组里找对应位置的值往往能还原出可读的字符串比如算法名、固定的盐值。第二控制流会被打乱用 switch-case 或者大量的 if-else 把顺序搅乱。这时候不要逐行读而是抓主干找到函数的入口和出口看它最终 return 的是什么中间那些花哨的结构先跳过。第三关键运算往往藏在不起眼的地方。比如一个看似普通的字符串拼接实际上是在拼接待加密的原文一个循环里的异或操作可能就是加密的核心。我的经验是重点关注字符串拼接、数组 join、以及任何涉及charCodeAt、fromCharCode、位运算的代码段。2.4 把关键函数抠出来单独跑找到疑似签名函数后最稳妥的验证方式是把它单独提取出来在 Node 环境里跑一遍看输出是否和浏览器里抓到的一致。提取的时候要注意它依赖了哪些外部变量——可能是全局对象上的某个配置、可能是另一个工具函数。把这些依赖一并抠出来或者用桩函数stub替代。这一步经常会失败报错说某个变量 undefined。别急着怀疑自己找错了函数先看报错指向哪个变量回到浏览器里看那个变量在断点时的实际值把它补上再试。这个补依赖的过程可能要来回几次但每补一个你对这个函数的理解就深一层。3. 拆解签名算法从现象反推原理函数抠出来了接下来要搞清楚它到底做了什么运算。这一章讲的是怎么从代码现象反推算法本质以及几种常见签名套路的识别方法。3.1 识别摘要类算法的特征如果签名结果是固定长度的十六进制串比如 32 位那大概率是 MD5 一类的摘要算法。识别方法很直接看代码里有没有出现初始化向量一堆固定的魔数、有没有分块处理的循环、有没有左移右移和加法混合的运算。MD5 的实现有几个标志性的常量看到它们基本就能确认。但要注意很多站点不会直接用裸的 MD5而是在原文里掺盐。盐可能是固定的字符串也可能是从某个配置里读的甚至可能是动态拼接的时间戳。所以还原的时候不能只还原算法还要还原到底对什么内容做了摘要。这一步错了算出来的 sign 永远对不上。我的验证方法是在浏览器断点处把传给摘要函数的原始字符串打印出来然后在本地用同样的算法对同样的字符串做运算对比结果。如果一致说明算法和输入都对了如果不一致先怀疑输入再怀疑算法。3.2 处理多字段拼接的顺序问题签名最常见的构造方式是把若干参数按某种规则排序拼成一个字符串再做摘要或加密。这里的魔鬼全在细节里——拼接的顺序、分隔符、是否包含空值、大小写处理任何一处不同结果就天差地别。我在处理这个场景时就栽过跟头。一开始我以为是把所有参数按字母序拼接算出来死活对不上。后来在断点里把拼接后的原始串打印出来一看发现它是按固定字段在前、动态字段在后的顺序拼的而且字段之间用的分隔符不是我想当然的而是一个特定字符。改过来之后一次就对了。所以我的建议是永远不要猜拼接规则直接在断点里把原始串打出来看。这一步能省掉你几个小时的试错。打印的方法很简单在拼接语句后面下断点看那个中间变量的值就行。3.3 时间戳与随机因子的作用很多签名里会掺入时间戳目的是让签名随时间变化防止重放。处理这类逻辑时要注意两点一是时间戳的精度秒级还是毫秒级二是它参与运算的位置是拼进原文还是作为单独的字段参与。还有一种情况是掺入随机数。随机数会让每次签名都不同但服务端验证时并不要求签名唯一只要求它能被正确验算。这种情况下还原的重点不是复现出和浏览器一模一样的签名而是复现出能被服务端接受的签名。这两者的区别很关键——前者要求你连随机数种子都一样几乎不可能后者只要求算法正确。判断属于哪种情况的方法连续抓几次请求看服务端是否都接受。如果签名每次都不同但请求都成功说明是后者你只需要还原算法随机因子随便取。3.4 用对照实验锁定每个运算环节当算法比较复杂时我习惯用对照实验的方法逐步锁定。具体做法是构造一组已知的输入在浏览器里跑出输出然后在本地复现每次只改一个环节看输出怎么变。比如怀疑某个环节做了大小写转换那就把输入全换成大写再跑看输出是否相应变化怀疑某处做了 URL 编码就构造带特殊字符的输入去试探。这种控制变量的思路比死读代码要快得多尤其适合那些被混淆得面目全非的运算。4. 本地复现让脚本跑出和服务端握手成功的结果算法搞清楚了最后一公里是把它落地成一个能实际发请求的脚本。这一章讲复现过程中的工程细节和验证方法。4.1 搭建最小可运行环境我用 Node 环境做复现因为它跑 JS 最直接不需要浏览器。核心依赖通常就一两个一个是 HTTP 请求库用内置的 https 模块或者 axios 都行另一个是加密库Node 内置的 crypto 模块基本够用MD5、SHA、AES 都有。环境搭好后的第一件事不是急着发请求而是先做纯函数验证把签名函数单独跑输入一组固定的参数看输出是否和浏览器里抓到的一致。这一步过了才说明算法还原正确过不了后面发请求全是白搭。// 纯函数验证输入固定参数对比输出 const crypto require(crypto); function buildSign(params) { // 这里的拼接规则来自断点观察到的实际顺序 const raw ${params.fixedField}${params.query}${params.ts}; return crypto.createHash(md5).update(raw).digest(hex); } const testParams { fixedField: abc, query: hello, ts: 1700000000000 }; console.log(buildSign(testParams)); // 与浏览器断点处抓到的 sign 对比一致则通过4.2 处理请求头与 Cookie 的连带问题签名对了请求不一定成功。我踩过的一个坑是服务端除了校验签名还校验了请求头里的某些字段比如来源标识、客户端类型。这些字段在浏览器里是自动带的用脚本发请求时如果漏了会被直接拒绝。排查方法很朴素把浏览器里那个成功请求的完整请求头复制下来逐条对比脚本发出的请求头缺什么补什么。Cookie 也是同理有些站点会把一部分状态放在 Cookie 里脚本需要先请求一次首页拿到 Cookie再带着它去请求接口。提示请求头里的字段名大小写、顺序在某些严格校验的场景下也可能有影响虽然少见但排查时可以留意。4.3 验证与失败排查的完整链路发请求失败时不要盲目改代码按链路排查先看 HTTP 状态码4xx 多半是参数或签名问题5xx 是服务端问题再看返回体里的错误信息很多接口会明确告诉你签名错误还是参数缺失。如果提示签名错误回到纯函数验证那一步重新对比原始串。我遇到过的签名对不上的原因排个序大概是拼接顺序错、分隔符错、时间戳精度错、编码方式错比如中文没做 UTF-8 处理、大小写错。按这个顺序查命中率很高。如果签名对但请求还是失败重点查请求头和 Cookie。还有一种隐蔽的情况是参数被二次编码——浏览器发出去之前对某些字段做了 encodeURIComponent脚本里如果没做或者做了两次服务端解析出来的原文就不对签名自然对不上。4.4 控制频率与合规使用脚本跑通之后我要泼一盆冷水能跑通不等于可以随便跑。翻译类接口通常有频率限制短时间大量请求会触发风控轻则返回错误重则封禁来源。做技术验证时我一般控制在每分钟个位数请求且只翻译自己需要的内容。更重要的是心态这套流程的价值在于理解前端加密的构造思路从而在自己做系统时知道怎么设计更合理的签名机制、怎么防止参数被篡改。把它当成一门防守方的必修课来学比单纯追求能调通接口有意义得多。5. 几个让我卡了半天的真实细节前面讲的是方法论这一章专门讲那些看文档永远看不到、只有自己动手才会撞上的细节。这些才是逆向分析里最耗时间、也最能拉开水平差距的部分。5.1 断点打不中的三种原因明明代码就在那断点却死活不触发这种情况我遇到过不止一次。原因通常有三类第一代码根本没执行到那里。可能是走了另一个分支或者那段逻辑是懒加载的需要特定操作才触发。解决办法是在更上游的地方下断点确认执行路径。第二断点被跳过了。有些代码在 Web Worker 里跑主线程的断点管不到还有些是内联在 eval 里的代码需要在特定的执行上下文里才能断住。遇到这种情况去 Sources 面板的对应上下文里找。第三代码被缓存了。浏览器可能加载的是旧版本你改的断点位置和实际运行的代码对不上。清缓存、强制刷新或者开无痕窗口重新来一遍。5.2 混淆代码里的假动作有些混淆手段会故意制造干扰。比如放一堆永远不会执行的分支里面写着看起来很像加密逻辑的代码或者把真正的运算拆散到多个函数里中间用全局变量传递。识别假动作的方法是看数据流。真正参与运算的变量它的值会一路流向最终的返回值假动作里的变量算完就扔了不影响输出。我一般的做法是从返回值倒推找到 return 语句看它返回的变量是从哪来的一路往上追追到源头就是真正的运算链。这条链之外的代码再花哨也可以先忽略。5.3 编码问题导致的差一个字符签名对不上排查半天发现是编码问题这种经历太常见了。中文、特殊符号、空格在不同的编码处理下字节序列完全不同。浏览器里 JS 的字符串是 UTF-16做摘要前通常会转成 UTF-8 字节如果你的脚本里直接用字符串做运算结果就会差。解决办法是统一在字节层面处理把字符串用Buffer.from(str, utf8)转成字节再喂给摘要函数。另外注意 URL 编码的时机encodeURIComponent 和 encodeURI 的行为不一样用错了也会导致原文不一致。5.4 时间戳精度与服务器时区时间戳参与签名时精度错了签名必错。秒级和毫秒级差三个数量级一眼能看出来但还有一种隐蔽的情况服务器用的是它自己的时间而不是客户端时间。如果你的本地时间和服务器有偏差算出来的签名可能落在服务端的容差窗口之外。排查方法是抓一个成功请求看它的时间戳和你的本地时间差多少。如果差得明显说明要用服务器时间那就得先请求一个返回服务器时间的接口用那个时间来做签名。这个坑我在处理带时效签名的场景时踩过卡了很久才想到是时间源的问题。6. 从这次分析里沉淀下来的通用套路做完一个案例如果不把方法沉淀下来下次遇到新的站点还是从零开始。这一章我把这次分析里可复用的思路提炼一下算是给自己也给读者留个检查清单。6.1 一套可复用的定位流程我把整个定位流程压缩成五步遇到新的签名场景可以直接套抓包对比连续抓多次请求找出每次变化且长度固定的字段锁定目标。全局搜索用字段名或接口路径搜代码缩小范围到具体文件。断点回溯在请求发送处下断沿调用栈往上找生成逻辑。提取验证把关键函数抠出来在本地用固定输入验证输出。对照排查不一致时用控制变量法逐个环节排查。这五步里第三步和第四步是最花时间的也是最需要耐心的。新手往往在第三步就放弃其实只要坚持往上翻几帧答案通常就在眼前。6.2 常见签名构造方式的识别速查不同的签名构造方式有不同的指纹我整理了一张速查表看到特征就能大致判断方向输出特征可能的构造方式验证切入点32 位十六进制MD5 摘要找魔数常量、看原文拼接40 位十六进制SHA-1 摘要同上注意初始化向量不同64 位十六进制SHA-256 摘要同上Base64 长串对称加密或编码看是否有密钥、IV混合大小写字母数字自定义编码或多次变换从返回值倒推数据流这张表不是万能的但能帮你在看到输出时快速形成假设少走弯路。假设错了就修正逆向本来就是个不断试错的过程。6.3 给做防守方的启示分析别人的签名机制最终目的是把自己的系统做得更稳。从这次分析里我总结出几条做接口防护的经验签名原文里一定要掺入服务端可控的因子比如服务端下发的一次性 nonce防止客户端伪造时间戳要配合合理的容差窗口既防重放又不误伤网络延迟关键参数要做服务端二次校验不能只信客户端算出来的签名。还有一点很重要不要把安全完全寄托在前端混淆上。混淆只能提高分析成本不能真正保密。真正的防护在服务端——限流、行为分析、异常检测这些才是主力。前端签名的作用是提高门槛、过滤掉大部分低级脚本认清这个定位设计时才不会本末倒置。我个人在实际操作中的体会是这类分析最锻炼的不是写代码的能力而是在信息不完整的情况下做假设、验证假设、修正假设的思维能力。你面对的是一个被刻意打乱的黑盒唯一的武器就是观察和实验。这个能力迁移到日常开发里就是排查线上问题、定位性能瓶颈时的那股顺藤摸瓜的劲头。所以哪怕你暂时用不上逆向分析拿一个公开站点练练手对提升工程直觉也是有好处的。最后再分享一个小技巧分析过程中随手记笔记把每个假设、每次验证的结果都写下来因为这类工作上下文切换频繁不记的话回头就忘了自己试到哪一步了。