news 2026/10/17 20:54:56

模拟天气接口JS反爬破解:签名算法定位与Python还原实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
模拟天气接口JS反爬破解:签名算法定位与Python还原实战

做爬虫开发或者接口联调的朋友,八成遇到过一种很烦的情况:浏览器里打开页面一切正常,数据清清楚楚,可一旦把同样的请求复制到代码里,马上被拒。最近我整理的这个“某天气网站JS反爬加密破解”项目,就是在模拟这种场景——一个带签名校验的天气接口,前端JavaScript负责生成加密参数,后端只认带正确签名的请求。表面叫破解,实际上做的是纯正向的代码分析:把JS里的加密函数找出来,搞清楚参数拼接规则,再用Python或Node还原同一套逻辑。适合刚开始接触JS逆向的爬虫开发、想了解接口签名设计的前端,以及做安全测试的人参考。

这里要先把边界说清楚:我不会拿任何真实在运行的天气服务当靶子。下面提到的“某天气网站”,就是一个我自己搭的模拟接口,请求路径、加密方式都是故意设计出来的,目的是把JS反爬常见的思路完整演示一遍。只要你懂一点浏览器开发者工具,有一点JavaScript基础,就能跟上。

1. 项目背景与整体思路

1.1 为什么天气类接口也要上加密

很多人觉得天气数据是公开的,没必要做反爬。但在实际业务里,天气数据会被大量聚合到各种App、小程序、大屏展示系统里。一旦接口地址公开,脚本可以在短时间内发起海量请求,服务端流量成本、计算成本都会上升,数据本身也容易被批量抓走再加工。

所以不少接口服务会采用“签名参数”的方式。客户端请求时,除了带上业务参数,还要额外生成一个sign,服务端用相同的算法再算一遍,一致才放行。这样一来,没有正确签名的请求会在入口被挡掉。这个sign通常由几部分组成:时间戳、随机数、业务参数、甚至固定的salt。想要伪造请求,就得先弄清楚JS里到底把这几个值怎么拼在一起的。

这个项目的学习价值就在这里:不是记住某一个网站的加密结果,而是掌握一套“从请求出发、反推加密逻辑、再用另一种语言重写”的方法。这套方法放到任何带签名参数的接口上都适用。

1.2 项目整体目标与技术选型

给这个项目定目标时,我把它拆成了四个阶段:

  1. 在本地搭建一个带JS签名校验的“天气接口”,同时准备一个模拟前端页面。
  2. 用浏览器开发者工具,从网络请求里找到加密参数。
  3. 通过全局搜索和断点调试定位前端生成签名的函数。
  4. 用Python还原签名算法,最终用脚本请求模拟接口并拿到天气数据。

工具选型很简单:浏览器就用Chrome或Edge的DevTools;抓包工具我平时用Charles,但这里直接看浏览器的Network面板就够;还原算法用Python 3,再加一个Node.js环境用来单步执行JS片段。为什么不一开始就上自动化工具?因为这个场景的核心在“分析”而不是“批量采集”,手动手工点一遍,反而能更快建立直觉。

在动手之前,我还特意把模拟接口的逻辑写得尽可能贴近真实情况:参数排序、加盐、时间戳验证都有。后面你就会发现,真实项目里踩到的坑,在这个模拟项目里基本上都能踩一遍。

2. 核心加密点拆解

2.1 第一步:把请求链路理清楚

拿到一个加密接口,第一件事不是去看JS代码,而是先把请求长什么样看清楚。打开浏览器开发者工具,切到Network面板,勾选Preserve log,然后刷新页面,找到天气数据这个XHR请求。

模拟请求大概是这样的:

GET /api/weather?cityName=xxx&ts=1700000000000&nonce=ab12cd&sign=7a3f5b...

其中cityName是业务参数,ts是毫秒级时间戳,nonce是随机字符串,sign是一串32位的十六进制字符。第一眼看到32位,基本能猜到是MD5,但具体加没加盐、参数怎么排序,还得靠代码确认。

这里有个习惯:不要直接复制整个URL到代码里测,先把参数逐项记下来。因为不同接口对参数顺序、大小写、是否编码有完全不同的要求。尤其是在后面还原算法时,“最终拼出来用于签名的原始字符串”和“URL上看到的参数”往往是两回事。

2.2 第二步:快速判断加密算法类型

类型判断是逆向里最提效的一步。见到一串固定长度的字符,我通常会先做一个初步判断:

特征大概率算法备注
32位十六进制MD5也可能自定义哈希
40位十六进制SHA1比较少见
64位十六进制SHA256越来越常见
以U2FsdGVkX1开头CryptoJS AES这是OpenSSL格式的固定前缀
密文含大小写字母、数字、+ /Base64或其变种可能是AES/RSA的密文载体
长度128/256位、有Padding特征RSA前端常用JSEncrypt加密
纯数字且较长时间戳/随机数编码不一定是哈希

这个表只能用于缩小范围,不能作为最终结论。比如有些团队会把MD5输出再Base64一次,长度就会变化;有的自定义算法可能故意把MD5前缀改掉。所以我都是把这张表当成“下一步往哪排查”的导航,而不是终点。

拿到sign参数后,如果长度是32位,我建议先去Sources面板里搜一下有没有CryptoJS的完整代码。搜"MD5"或者"hex"都可能命中。如果搜不到,就直接跳到XHR断点。

2.3 第三步:用断点确认加密函数

定位加密函数的效率,直接决定了整个项目的工期。我最常用的是三个手段,从快到慢排列:

  1. XHR/fetch断点:在Network里右键目标请求,选择Break on XHR/fetch,刷新页面,当请求发出前JS会停在发送位置。这时从调用栈往上一层一层点,很快就能找到真正搞加密的函数。这是最稳的一招。
  2. 全局搜索:在Sources里按Ctrl+Shift+F,输入sign=或者特定的请求URL片段。如果加密代码没有被混淆,通常直接能命中。如果命中的是压缩过的单行文件,就格式化后再搜。
  3. 调用栈回溯:断点触发后看Call Stack面板,往下找离加密逻辑最近的函数,再在左边Scope面板里看局部变量。比如你看到某个函数里出现了一个叫t的变量,值是32位的字符串,那大概率就是它。

这三个手段不是孤立的。通常我是先加XHR断点,再用调用栈回溯到加密函数,然后在函数体内下普通断点,单步执行,查看每一步的输入输出。整个过程的核心目标只有一个:找到“原始字符串的拼接规则”。

有不少朋友卡在“不知道从哪下手”,其实问题不是技术,而是顺序:先确定断点位置,再分析拼接规则,最后才是还原算法。顺序对了,基本不会迷路。

3. 实操过程:从拿到加密参数到完整还原

3.1 搭一个模拟天气接口

为了不触碰任何真实业务,我直接用Node.js写了一个带签名校验的简易接口。后端逻辑很简单,收到请求后把参数按字典序排序、拼上固定salt、再计算MD5,比对请求里的sign是否一致。

// server.js const express = require('express'); const crypto = require('crypto'); const app = express(); const SALT = 's3cret_for_test'; function calcSign(params) { const keys = Object.keys(params).sort(); const raw = keys.map(k => `${k}=${params[k]}`).join('&') + `&salt=${SALT}`; return crypto.createHash('md5').update(raw).digest('hex'); } app.get('/api/weather', (req, res) => { const { cityName, ts, nonce, sign } = req.query; const params = { cityName, ts, nonce }; const expected = calcSign(params); if (sign !== expected) { return res.status(401).json({ code: 401, msg: 'sign invalid' }); } res.json({ code: 0, data: { city: cityName, temp: '26℃', humidity: 60 } }); }); app.listen(3000);

配套的页面里,前端JS负责生成sign请求。核心生成逻辑同样放在script里:

// 模拟页面里的加密逻辑 function createSign(params) { const salt = 's3cret_for_test'; const keys = Object.keys(params).sort(); const raw = keys.map(k => `${k}=${params[k]}`).join('&') + `&salt=${salt}`; return md5(raw); // 假设md5由工具库提供 }

为什么要自己搭?因为真实业务里你没法随便断点、随便打印,但在自己搭的环境里可以反复折腾,把所有边界情况都试一遍。很多团队做安全测试时也是先造一个靶场,跑通方法后再去检查自己的线上系统,这是一种很稳妥的做法。

3.2 从请求细节反推签名规则

把页面跑起来,打开开发者工具,抓到的请求大概长这样:

/api/weather?cityName=某城市&nonce=4f7a9c&ts=1700000000000&sign=1f6c4f...

注意URL里的参数顺序是cityName、nonce、ts,但签名函数里用的是字典序。这就是为什么不能只看URL顺序,一定要去JS里确认拼接顺序。

通过XHR断点和调用栈,很快定位到了生成sign的createSign函数。在函数里下个断点,观察raw的值,发现它其实是:

cityName=某城市&nonce=4f7a9c&ts=1700000000000&salt=s3cret_for_test

也就是说:参数先按字典序排序,每一项用k=v的形式拼起来,用&连接,最后再在末尾追加salt。然后对整个字符串做一次MD5,转成32位十六进制。到这里,加密逻辑已经非常清楚了。

这里有个容易忽略的细节:真实接口里可能只对特定参数签名,空值参数、userId这类没传的参数要不要参与拼接,都需要在断点里逐个确认。有些接口还会把时间戳从毫秒转成秒,或者用encodeURIComponent处理一次。如果还原时漏了任何一步,结果就完全不同。

3.3 用Python还原签名算法

明确了规则,用Python复现就很快。我按常用方式写了一个函数:

import hashlib import time def make_sign(city_name: str, ts: str, nonce: str, salt: str) -> str: params = { "cityName": city_name, "ts": ts, "nonce": nonce, } keys = sorted(params.keys()) raw = "&".join(f"{k}={params[k]}" for k in keys) + f"&salt={salt}" return hashlib.md5(raw.encode("utf-8")).hexdigest() if __name__ == "__main__": city = "某城市" ts = str(int(time.time() * 1000)) nonce = "4f7a9c" token = make_sign(city, ts, nonce, "s3cret_for_test") print(token)

Python里特别要注意编码问题。如果JS里用的是encodeURIComponent,那么中文城市名会被转成百分号编码,Python这边必须用urllib.parse.quote做同样处理;如果JS里用了unescape,则要反过来解码。这一步对不上,其他全白搭。

下面写一个测试请求的脚本,验证整个链路是否打通:

import requests import time city = "某城市" ts = str(int(time.time() * 1000)) nonce = "abc123" sign = make_sign(city, ts, nonce, "s3cret_for_test") resp = requests.get( "http://127.0.0.1:3000/api/weather", params={"cityName": city, "ts": ts, "nonce": nonce, "sign": sign}, timeout=5, ) print(resp.json())

如果一切正常,会返回天气信息。如果返回sign invalid,优先检查拼接的raw字符串与浏览器里是否完全一致。此时可以在Python打印raw,再回到浏览器断点里比对。这个方法虽然笨,但确是最有效的排查方式。

3.4 如果加密从MD5变成AES

MD5是最简单的情况。真实项目里更常见的是AES或RSA。AES的核心是知道key、iv、模式和填充方式。在JS里如果看到CryptoJS.AES.encrypt,默认输出是OpenSSL格式,也就是以U2FsdGVkX1开头的字符串。它把salt随机生成后和密文放在一起,所以不能直接拿整段字符串去还原。

碰到AES时,我会按这个顺序排查:

  1. 搜索CryptoJS.AES.encrypt调用,看key从哪里来。可能是写死的字符串,也可能是从某个请求返回的。
  2. 确认iv是否指定。没指定的话,CryptoJS会用默认的零向量,并且如果key是字符串,会通过OpenSSL的EVP_BytesToKey派生真正的key。
  3. 确认输出格式。toString()默认是OpenSSL Base64,换成hex则输出十六进制。

补一个Python解AES的例子:

from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad import base64 key = b"1234567890abcdef" iv = b"abcdef1234567890" cipher = AES.new(key, AES.MODE_CBC, iv=iv) # 加密 ciphertext = cipher.encrypt(pad(b"hello", AES.block_size)) print(base64.b64encode(ciphertext).decode()) # 解密 cipher = AES.new(key, AES.MODE_CBC, iv=iv) plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size) print(plaintext.decode())

需要注意的是,Python的AES默认不对明文做padding,所以必须手动用pad补到16的倍数;JS里的CryptoJS则默认做PKCS7填充,所以两边结果才能对上。

3.5 遇到环境检测怎么办

有些前端加密逻辑会依赖window、document、navigator这些浏览器环境。常见的做法是在Node里先构造一个假环境,或者用jsdom。遇到这种情况,不要急于去模拟所有API,而是先看代码到底用了哪些方法。比如只用了Date和Math,就直接在Node里补这两个对象属性即可;如果用了document.cookie,则需要引入jsdom。

我常用的一个笨办法是:在浏览器控制台里把相关对象的JSON结构打出来,看它在代码里被读取了哪些字段。然后用Node的global对象把字段补齐。如果只是少数几个字段,根本不需要引大型库。

如果目标加密函数很长,也可以直接把它复制出来单独跑。推荐用Node.js而不是pyexecjs,因为Node对原生JS的兼容性更好,调试时还能用console.log。

4. 实战中容易踩的坑

4.1 搜索关键词找不到加密函数

这是最普遍的一个问题。加密逻辑可能在一个经过混淆的大文件里,变量名全是a、b、c,或者用eval动态拼接,直接搜sign会搜不到。

我自己的搜索顺序是:先搜请求URL里的固定路径,比如/api/weather;再搜sign=或sign:,不行就搜参数名cityName。如果都搜不到,就直接加XHR断点,刷新页面,让断点带你到请求发起处,再往调用栈上层找。路径搜索和断点互为一组双保险,配合起来基本不会漏。

另外,现在的混淆工具会把字符串做Unicode编码,搜索中文反而是无效的。所以优先搜索ASCII可见字符,比如引号、等号、JSON字段名。

4.2 复现结果和浏览器不一致的N个原因

我见过太多人卡在这一步:算法明明看懂了,本地算出的sign就是不对。常见原因可以按命中频率排名:

  1. 参数排序方式不对。有的按ASCII字典序,有的按固定数组顺序,还有的按参数名长度排序。
  2. 字符串编码不一致。中文没有编码,MD5出来肯定不同。
  3. 时间戳单位不对。有的用秒,有的用毫秒,有的固定10位,有的是13位。
  4. 空参数是否参与拼接。参数不存在和参数值为空字符串,在JS里是两种不同的处理方式。
  5. 随机数算法不一致。某些加密会把随机数作为salt的一部分,而浏览器每次生成的随机数不同,签名自然不同。
  6. 大小写问题。MD5输出通常是小写,但如果JS做了toUpperCase,Python没有做,就会失配。

遇到不一致,我的习惯是打印raw字符串做对比。在浏览器断点处复制raw,在Python里打印raw,两个字符串一个字符一个字符地核对,所有问题都能看出来。

4.3 调试过程中遇到反调试

有的前端代码会检测开发者工具是否打开,检测到就进入死循环或者内存溢出,干扰正常分析。对应的简单处理包括:禁用断点后快速跳过、使用无痕窗口、或者在代码里找到检测点直接改掉。

这个项目因为是模拟站,我没有刻意加反调试。但在真实授权测试里遇到此类问题时,核心思路是“先找到检测代码,再考虑绕过”,而不是一上来就开各种插件。检测代码往往就藏在某个定时器或事件监听里,通过搜索关键词,比如debugger、performance、timing,可以快速定位。

4.4 签名时效性与重放限制

很多带签名的接口会校验时间戳的合法性,比如服务器认为五分钟前的请求无效。所以脚本不能写死一个sign,每次请求前都要动态生成。同时,某些接口还会校验nonce是否已经被使用过,防止重放攻击。这种情况下,除了生成正确签名,还要保证每次请求都用新的nonce。

平时调试时可以直接用浏览器里的参数验证脚本;但真正接入数据流程时,必须把生成过程和实际请求放在同一个循环里。这也是这个模拟项目最后验证的部分,相当于把整套签名流程变成一个可复用的函数。

5. 这类项目带来的能力延伸与合规边界

5.1 做完项目你能获得什么

从一个“抄请求都会失败”的状态,到能自己分析加密参数并还原签名,这个转变带来的不只是某个接口的通路。你会开始理解前端代码混淆、参数传递、服务端校验这一整套链路;也会知道“前端加密”从来不是一个安全边界,它只是提高门槛。

如果你负责接口设计,做完这个项目后自然会想到:密钥不能写在前端、签名校验要放在服务端、重要业务还要配合频率限制和风控。所以这件事反过来对开发者有很强的参考意义。

5.2 合规边界要时刻清楚

最后必须强调一个原则:本文所有分析都基于自建的模拟接口。实际工作中,如果要对某个线上系统做安全评估,必须先获得明确授权;否则无论出于什么目的,对未授权系统进行爬取、破解、干扰,都可能触碰法律红线。

即使是在授权范围内,也要遵循几个基本约定:不要高频率请求,避免影响正常服务;不抓取非必要数据;不把破解方法用于商业竞争。技术本身是中性的,但使用场景决定了它是否合规。

我做这个项目时,给自己定的标准是“能演示、能教学、不碰真实服务”。希望看到这篇文章的人也能保持同样的边界感。

做了几次类似的模拟项目之后,我最大的体会是:JS反爬加密破解,真正难的不是加密算法本身,而是能不能静下心把“请求参数分析、断点定位、算法还原”这条链完整走通。只要你把方法练成一种肌肉记忆,以后再遇到新的签名参数,无非是换一个函数名、换一种编码方式而已。最后再说一个小技巧:每次逆向完,记得把参数拼接规则、salt位置、时间戳单位整理成一份Markdown笔记,下次遇到同类型的网站,直接参照笔记能节省大量时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 20:53:11

基于知识图谱的智能教学推荐系统实战:从建模到部署

简介:本资源是一份聚焦教育智能化落地的深度技术文档,面向教育技术研究者、智能教育产品开发者及高校教育信息化实践者,解决传统教学中个性化不足、知识关联弱、学情诊断粗放等核心问题。文档系统阐述了基于知识图谱的个性化智能教学推荐系统…

作者头像 李华
网站建设 2026/10/17 20:52:19

软考中级考点笔记全解析:数据结构、排序与高频易错点

简介:软件设计师中级考点笔记精华版定位为软考中级考证冲刺资料,面向正在系统复习数据结构、算法与程序设计的考生。内容聚焦考试常考的核心知识点、难点公式与易错陷阱,将邻接矩阵、顺序/链式/散列/索引存储结构、满二叉树、AVL树、哈夫曼树…

作者头像 李华
网站建设 2026/10/17 20:50:36

大模型推理的历史数据管理:上下文、KV Cache与显存优化

大模型推理这事,聊到“历史数据”,很多人的第一反应是训练集,是微调语料。但今天我想聊的是另一个维度:推理阶段,大模型自己是怎么面对“历史”的。这个“历史”,既包括一次请求里你喂给它的前文上下文&…

作者头像 李华
网站建设 2026/10/17 20:50:32

AI Agent记忆系统设计拆解:从51万行源码看上下文管理

老实说,看到“某AI编程助手CLI源码曝光”这个消息的时候,我第一反应不是去吃瓜,而是赶紧去扒它在记忆管理上的设计。干AI Agent开发这一行,大家心里都清楚:模型能力决定天花板,记忆系统决定地板。51万行代码…

作者头像 李华
网站建设 2026/10/17 20:43:22

Kafka接入AI实战:从异步削峰到Agent事件流与性能排查

Kafka接入AI这件事,最近在圈子里讨论热度确实高。我所在的团队上个月也正式完成了Kafka与AI服务链路的全量对接,从最初只敢拿它承接日志数据,到现在AI推理请求、Agent事件流、模型训练样本全都跑在Kafka上,整个过程踩了不少坑&…

作者头像 李华
网站建设 2026/10/17 20:41:47

C++用EasyX开发贪吃蛇:从数据结构到防闪烁完整实战

早几年我做 C 图形界面练习的时候,顺手用 EasyX 写过一版贪吃蛇。后来陆陆续续有人拿着类似的“高仿贪吃蛇”源码来问我,说为什么别人的版本能做到经典掌机那种手感,自己照着抄却总是闪屏、卡顿、按一下方向就原地暴毙。这个项目看上去小&…

作者头像 李华