爬虫逆向实战:从抓包定位到Python还原加密签名参数
2026/9/19 6:27:59 网站建设 项目流程

做爬虫最怕的不是请求失败,而是参数做得像一堵墙。我前两天在抓央视频某个内容接口时,就撞上了这样一堵墙:直接请求业务接口,服务器返回的是{"code":400,"msg":"invalid sign"},可在 App 里明明一切正常。把同一个请求参数原封不动复制到 Postman 里重新发,依然报错,说明这些参数里藏着动态加密内容,而且很可能和请求时间绑定了。

这篇文章就聊聊我是怎么从抓包定位、JS 断点调试,到最后用 Python 还原加密参数、跑通完整请求的。整个过程不依赖付费软件,也不需要逆向工程里那些花哨的 hook 框架,只要会基本的抓包和 Python 语法就能跟下来。适合那些刚开始接触爬虫逆向、被各种signtsdevice_id参数折磨得一头雾水的朋友。

1. 先把目标参数“逮”出来:抓包与关键词定位

做加密参数逆向,第一步从来不是打开代码编辑器,而是先把“这个参数到底长什么样”搞清楚。任何加密算法最终都要落到 HTTP 请求上,抓包看到的就是加密参数最终的样子,这一步差不了,后面才不会白忙。

1.1 环境准备:代理抓包与 HTTPS 证书

抓包工具我习惯用 Charles,Fiddler 也行,如果你更偏好命令行,mitmproxy 也可以。我的组合是 Android 模拟器 + Charles:模拟器里把 Wi-Fi 代理指向电脑的 8888 端口,电脑端打开 Proxy Settings,勾选启用 SSL Proxying,然后把 Charles 生成的证书安装到模拟器里。具体步骤如下:

  1. 电脑和模拟器处于同一局域网。
  2. 模拟器中 Wi-Fi 长按修改网络,设置手动代理,主机写电脑局域网 IP,端口写 8888。
  3. 访问chls.pro/ssl下载并安装证书。
  4. Charles 的 Proxy -> SSL Proxying Settings 里添加*:443,把 HTTPS 流量解开。

这里要提醒一句:Android 7.0 以上很多 App 默认不信任用户证书,抓 HTTPS 会看到一堆Client SSL handshake failed。解决方式有几个:用 Android 7.0 以下的老版本模拟器、把证书装进系统证书目录、或者干脆抓 Web 端和小程序端的接口来绕过 App 证书校验。我最开始就卡在这里,后来发现同款功能在央视频的小程序端也有,接口参数几乎一样,抓包难度瞬间降了一个等级。

1.2 从请求列表里区分哪些参数是动态的

抓包跑通之后,先在 App 里点几个页面,找到目标业务接口,然后盯着请求 URL 和 query 参数看。我拿到的请求长这样:

GET https://api.example.com/media/video/info?vid=885009 &deviceid=a1b2c3d4e5f67890 &appid=6001 &version=2.0.2 &ts=1735212364000 &sign=0e2f5a8c4b6d3f1a9c7e8b0d2f4a6c8e

这串参数里,vid是视频 ID,deviceid看着像固定设备标识,appidversion是常量,ts是毫秒时间戳——每次请求都会变,而sign是一段 32 位的十六进制字符串,凡是这种长度和字符集的,大概率是 MD5 的摘要。

为了确认哪些字段参与了加密,我把请求重新发一遍。第一次请求里的ts已经过期,重新提交还会报无效签名,说明服务端校验了时间窗口。如果只改deviceid,签名也失效,说明设备 ID 大概率也参与签名。用排除法缩小范围,最终目标就是回答一个问题:sign到底是由哪些字段、按照什么顺序、用什么算法生成的。

1.3 全局搜索:锁定签名生成代码

定位加密代码最快的方式不是逐条阅读 JS,而是直接在抓包结果里搜索关键词。加密参数在代码里必然有一个赋值过程,常见写法有:

  • sign = md5(...)
  • sign: CryptoJS.MD5(...)
  • params.sign = hexdigest(...)
  • setSign(...)

在 Charles 的 Search 功能里输入sign =sign:,定位到包含签名算法的 JS 文件。再打开这个 JS 文件,搜索md5sha1CryptoJShexdigest这些词,基本能看到签名函数。

有个小经验:Web 端和小程序端的 JS 往往比 App 原生包更容易定位。央视频小程序端是典型的前端打包产物,打开开发者工具的 Sources 面板,搜索后能直接跳到sign生成代码附近,它的缩进甚至没有做压缩,比直接在 App 里 hook 舒服太多。

2. 顺着 JS 执行栈找加密函数:断点调试实战

很多教程会直接告诉你“签名就是 MD5,直接拿 Python 算就行”,这样的结论对当下有效,却解决不了下次遇到新接口的问题。真正有价值的是顺着 JS 执行栈找到加密函数那一刻,你的思路和工具链条才算建立起来。

2.1 在 JS 代码里打断点,回追调用栈

拿到目标 JS 文件后,我不急着看完整内容,而是先搜索sign赋值的地方。打开 Chrome DevTools 的 Sources 面板,在左侧找到当前页面加载的 JS 文件,如果没有格式化,先点击左下角{}按钮美化。然后按Ctrl+Shift+F全局搜索sign =sign:,点击搜索结果跳到对应的行,在行号上单击打一个断点。

断点打完后回到页面触发一次业务请求。此时 DevTools 会自动停在断点位置,右侧 Call Stack 面板显示了完整的调用链,Scope 面板则能看到当前函数作用域内所有变量。我当时的调用链大致是:

fetchVideoInfo (media.js:482) -> handleRequest (utils.js:271) -> generateSign (security.js:136)

沿着调用栈往上翻,很快就能找到真正的签名函数位置。这种“断点往回追”的方式,比人肉阅读 JS 高效得多,尤其是遇到 Webpack 打包后的代码,变量名全是ten,直接读懂几乎不现实。

2.2 拆开 Webpack 打包逻辑:不需要全部看懂

央视频小程序端加载的 JS 通常经过 Webpack 打包,产物里会出现__webpack_require__这样的模块引用。很多初学者看到这串代码就头皮发麻,其实完全没必要。我们在断点处只需要关心当前这个函数在做什么,不用管它是怎么被加载进来的。

我碰到的情况是这样:generateSign函数接收一个对象,内部先取出对象的appiddeviceidversionts字段,把它们拼成一个字符串,再套一层md5。关键代码类似:

function generateSign(data) { var raw = 'appid=' + data.appid + '&deviceid=' + data.deviceid + '&version=' + data.version + '&ts=' + data.ts + '&key=4f5e...'; return md5(raw).toString(); }

注意这里拼参数字符串时用的是固定顺序,而且末尾拼接了一个看似随机的key。这个key就是常说的“盐”。签名算法本身并不复杂,复杂的是你必须确认:哪个字段在前、哪个字段在后、盐值是什么、字符串中间有没有其他符号。差一个字符,签名结果就完全不同,所以断点调试比猜重要得多。

2.3 参数组合规律分析:到底哪些字段参与了签名

把断点处的变量逐个记下来,回到代码里比对比对,通常参与签名的字段不会太多。常见组合有两种:

  • 简单拼接式:appid=xx&deviceid=xx&version=xx&ts=xx&key=盐
  • 字典排序式:先把参数按照 key 的字符顺序排序,再拼接,最后签名

判断是哪一种,直接看 JS 代码里有没有sort()Object.keys(...).sort()。我遇到的央视频接口属于第一种,顺序固定,不需要排序,这对 Python 还原来说特别友好。

把这些字段整理成一张表记录下来,后面写 Python 时就是照着这张表逐个取值:

字段名类型来源是否参与签名
vidstring业务参数
appidstring常量
deviceidstring设备标识
versionstring常量
tsstring毫秒时间戳
keystring固定盐值

3. 用 Python 把签名算法“翻译”出来

确认了签名算法是“固定顺序拼接 + MD5”之后,Python 还原其实只需要几十行代码。这个阶段的关键不是写代码,而是把 JavaScript 的字符串处理逻辑完整地映射到 Python 上。

3.1 先厘清参与签名的字段与编码

从 JS 断点里我拿到了完整字段顺序和盐值,接下来先做一步“模拟签名验证”:我把从断点里抄出来的各字段原值,按顺序拼接后手动算一个 MD5,和请求里的sign对比。如果一致,说明算法确认无误。

这一步要在正式写 Python 前做,避免辛辛苦苦写完代码,最后发现是参数字段漏了某一个。最简单的验证方式是在浏览器 Console 里直接执行这段 JS 签名函数,或者用 Node.js 跑一遍:

node -e "const md5=require('crypto').createHash('md5'); console.log(md5.update('appid=6001&deviceid=a1b2c3d4e5f67890&version=2.0.2&ts=1735212364000&key=4f5e...').digest('hex'))"

把输出结果和小程序请求头里的sign对比。如果一致,说明算法没问题;如果不一样,回到断点继续查字符串拼接格式。很多签名对不上的原因不是算法错了,而是ts用了秒而不是毫秒,或者字符串里多了个看不见的换行符。

3.2 核心签名函数:先看是 MD5 还是 HMAC

从 JS 代码中看到md5(...)就能确定是摘要算法。如果看到的是类似HmacSHA256(value, key),那就要用 Python 的hmac模块。两种写法差别不大,但要注意:

  • MD5 类的签名参数通常拼在原始字符串里;
  • HMAC 类的签名,密钥是独立参数,不会拼进明文字符串。

央视频这个接口属于前者。我写了一个通用的签名生成函数,方便后续参数调整时直接复用:

import time import hashlib import requests from urllib.parse import urlencode SALT = "4f5e..." # 从 JS 中提取的固定盐值,真实环境记得替换 def make_sign_v1(params: dict, salt: str) -> str: items = list(params.items()) # 这里保持 JS 验证时的顺序,不要随便排序 raw = "&".join(f"{k}={v}" for k, v in items) raw += f"&key={salt}" return hashlib.md5(raw.encode("utf-8")).hexdigest()

代码非常短,但要注意三点:第一,params里的键值顺序要和 JS 里拼接时的顺序完全一致;第二,拼接时是用&连接,还是纯字符串拼接,必须以 JS 代码为准;第三,编码统一用UTF-8

3.3 构造完整请求:签名和请求头一起处理

签名算法摸清以后,请求整个流程就顺了。以下是我最终跑通的完整代码,删掉了和具体业务强相关的部分,只保留核心结构:

import time import hashlib import requests SALT = "4f5e..." VIDEO_URL = "https://api.example.com/media/video/info" def generate_sign(params: dict) -> str: raw_string = "&".join(f"{k}={v}" for k, v in params.items()) + f"&key={SALT}" return hashlib.md5(raw_string.encode("utf-8")).hexdigest() def fetch_video_info(vid: str): # 取一次时间戳,后面签名和请求都用同一个 ts = str(int(time.time() * 1000)) params = { "vid": vid, "appid": "6001", "version": "2.0.2", "deviceid": "a1b2c3d4e5f67890", "ts": ts, } sign = generate_sign(params) params["sign"] = sign headers = { "User-Agent": "Mozilla/5.0 ...", "Referer": "https://servicewechat.com/", "Content-Type": "application/json", } resp = requests.get( VIDEO_URL, params=params, headers=headers, timeout=10, ) return resp.json() if __name__ == "__main__": print(fetch_video_info("885009"))

这套代码里每一部分都有明确作用:ts先存一次,保证签名和请求参数完全一致;params的字段顺序和参与签名的顺序保持一致,sign字段本身不再参与签名;请求头里的RefererUser-Agent来自抓包原始请求,避免被服务器判定为异常请求。

3.4 如果算法太重:用 execjs 直接执行 JS 兜底

有些接口的加密不只是一次 MD5,而是混杂了 AES、RSA 甚至自定义编码,Python 重写成本会直线上升。这时候我建议不要硬翻译,直接用 Python 调 Node.js 执行原始 JS 代码段。

import execjs js_code = """ function generateSign(data) { var raw = 'appid=' + data.appid + '&deviceid=' + data.deviceid + ... return md5(raw).toString(); } """ ctx = execjs.compile(js_code) sign = ctx.call("generateSign", { "appid": "6001", "deviceid": "a1b2c3d4e5f67890", "version": "2.0.2", "ts": "1735212364000", })

execjs 的思路是把 JS 代码当作黑盒,保留原有逻辑,避免翻译过程中引入差异。不过它有个明显缺点:需要本地有 Node.js 环境,执行速度也比纯 Python 慢。我的建议是,碰到 MD5/SHA 这类简单摘要,优先用 Python 重写;碰到 AES、RSA 或好几层自定义混淆时,再用 execjs 兜底。

4. 从签名到完整请求:集成时的细节与坑

签名函数写出来只是第一步,真正把整个请求流程跑稳定,中间还有一堆让人头大的小问题。这些问题刚开始看起来像加密算法有问题,最后发现基本都是“签名和请求不一致”或“请求头被校验”导致的。

4.1 时间戳先存一遍,不要在签名和请求中间再次调用

最容易出现的坑,就是签名用的时间戳和请求参数里的时间戳不是同一个。看下面这段错误示范:

params = { "ts": str(int(time.time() * 1000)), ... } sign = generate_sign(params) params["sign"] = sign # 看起来没问题?

如果generate_sign内部再次调用time.time()生成 ts,而 params 里的 ts 是之前生成的,那么服务端用 params 里的 ts 验签就永远对不上。你以为算法错了,其实是两个时间戳差了那么零点几秒。

正确做法是:签名前先把时间戳赋值给一个变量,签名和参数字典都用这个变量。

ts = str(int(time.time() * 1000)) params = {...}

4.2 参数拼接顺序、大小写与编码问题

MD5 的输入一旦改变,输出就完全变样。最容易中招的地方有三个:

  • 大小写:JS 里如果先toUpperCase()再拼接,Python 也要先统一转大写;
  • URL 编码:如果某个参数值是中文或特殊字符,JS 里可能出现encodeURIComponent,Python 里要对应使用urllib.parse.quote
  • 拼接符号:有的接口用&连接,有的直接纯字符串连续拼接,甚至还会在中间插入固定字符,这些必须以断点里的实际代码为准。

我自己踩过的坑是:JS 里拼接时用了+把字符串连接起来,而我在 Python 里用join时多加了&,导致整个签名错误。后来老老实实回到断点处把raw的实际值打印出来,和 Python 里生成的字符串做了一次逐位对比,才找到问题。

4.3 请求头里的 Referer、User-Agent、Cookie 不能乱填

签名验证通过之后,请求可能依然返回 403 或 412。这时候问题基本出现在请求头。

很多服务端会校验Referer是否合法、User-Agent是否像是真实客户端。央视频小程序端接口对Referer很敏感,直接发请求需要带上Referer: https://servicewechat.com/。如果你抓的是 App 接口,通常还需要带CookieAuthorization字段。

建议直接复制抓包时的原始请求头,逐个字段保留,不要嫌多。即使有些字段看起来没用,先留着也能减少变量。等确认能稳定跑通以后,再逐字段删减测试哪些可以省略。

4.4 验证“签名通过”后,还会遇到的风控

签名的本质是反篡改和防重放,但它不是唯一的安全手段。请求频率过高、设备 ID 频繁变化、IP 的并发量过大,都可能触发风控。

我在测试时遇到过一次比较典型的限制:连续请求十几条后,接口返回了{"code":429,"msg":"too many requests"}。这个状态码说明前面签名已经完全正确,只是请求频率太高。处理方式就是控制节奏,每次请求之间加一个随机延时:

import time import random for vid in vid_list: fetch_video_info(vid) time.sleep(random.uniform(1.5, 3.5))

这里想多说一句:不要在爬虫项目里无脑上高并发,尤其是对国内主流平台的接口,既不符合平台规则,也容易被封 IP。刘润那句话放在技术圈一样成立:慢慢来,比较快。

4.5 逆向代码的维护心态

加密参数还原不是一次性的工作。平台只要升级了前端代码,签名算法就可能变,今天能跑的代码,明天一个JS文件更新就废了。所以我把签名生成逻辑单独放在一个模块里,接口调用模块和签名模块解耦,等下次签名算法变了,只需要改签名模块。

更重要的是,每次逆向都要把抓包到定位的过程沉淀成文档。我当时记录了一份包含“参数表 + 算法验证方法 + 参考请求头”的小文档,下次平台更新,我只需要拿着文档重新对一遍字段顺序,很快就能恢复请求能力。这个习惯帮我节省了大量重复时间。

5. 写在最后:爬虫逆向的边界与安全提醒

技术层面聊得差不多了,最后说点更重要的。

逆向解析加密参数这件事,在很多技术社区里都处于灰色地带。从学习角度来说,分析签名算法、理解前端加密逻辑,能帮你深入理解 HTTP 通信、反爬设计、签名机制,这些能力在开发高可用系统时非常有用。但从合规角度来说,未经授权地批量采集平台数据、绕过访问控制,可能带来法律风险。

我个人的习惯是:除非有明确授权,否则只把逆向当作学习手段,不做大规模采集,更不会把采集到的数据用于商业用途。如果你想练习这类逆向,建议选择自己开发的服务端接口做实验,或者只在本地模拟微信小程序环境测试签名逻辑,效果是一样的。

回到技术本身。央视频这个接口的加密强度并不算高,搞懂它更像是一道“逆向入门练习题”。真正难的是后续平台升级、参数变化、混合加密、反调试这些进阶内容,但只要把“抓包定位 -> 断点追栈 -> Python 还原 -> 请求验证”这套方法练熟了,再复杂的东西也能一点一点啃下来。

最后分享一个我常用的调试小技巧:在 Python 里生成签名后,先把整个 URL 完整打印出来,放到浏览器地址栏里直接访问。如果浏览器端能拿到数据,说明参数和签名都是正确的;如果不行,就说明还有请求头校验。这个技巧帮我在无数个抓包深夜里省下了整整一小时。希望它也能帮到你。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询