ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多平台爬虫 a_bogus 参数逆向:从抓包到本地复现的完整链路

多平台爬虫 a_bogus 参数逆向:从抓包到本地复现的完整链路

简介:这份资源面向从事多平台爬虫开发的中高级工程师,聚焦抖音、小红书、快手等平台中a_bogus参数加密的逆向与通用解法。内容围绕加密参数的共性与差异识别、跨平台补环境方案、环境检测分层设计、动态代理技术,以及从抖音到小红书的算法逻辑迁移与适配展开,并给出可复用多平台签名生成库的构建思路,将平台差异抽象为可注册策略对象,通过统一调度接口管理。资源包共3个文件,包含inscode工程配置、html页面与gitignore忽略规则,整体约11KB,结构轻量,便于快速导入与二次开发。已有116人学习下载,适合希望掌握参数逆向、混淆处理与算法变种识别技巧的开发者参考,可帮助理解加密入口定位、参数映射及跨平台适配的完整链路。

1. 多平台爬虫 a_bogus 参数逆向:从抓包到本地复现的完整链路

做数据采集的同行大概率都遇到过这种场景:接口地址、请求头、Cookie 全都对上了,返回的却是空数据或者一串风控提示。问题往往不在你的请求逻辑,而在一个叫a_bogus的查询参数上。它是某短视频平台 Web 端接口的签名参数,长度通常在 100 到 200 字符之间,由前端 JS 在发请求前动态生成,服务端校验不通过就直接拒绝。这份代码包做的就是把这个参数的生成逻辑从混淆过的 JS 里逆向出来,用 Python 在本地复现,让采集脚本不再依赖浏览器环境。适合有 Python 基础、做过接口采集、想搞清楚签名参数怎么落地的人。下面按「它是什么 → 怎么跑通 → 坑在哪 → 怎么验证」的顺序拆开讲。

2. a_bogus 的生成链路:从浏览器执行到 Python 复现

2.1 先搞清楚这个参数到底由什么决定

逆向签名参数最忌讳一上来就啃混淆代码。我一般先做一件事:固定其他变量,只改一个输入,看输出怎么变。具体做法是在浏览器里反复调用同一个接口,每次只改一个字段,对比a_bogus的值。几轮下来就能确定它的输入集合大致包括:请求 URL 的路径部分、查询字符串(不含a_bogus本身)、请求体的 MD5、时间戳、以及一个从 Cookie 或 localStorage 里读出来的环境值。

这里有个容易翻车的地方:很多人以为签名只跟 URL 有关,结果发现同样的 URL 换个请求体,签名就对不上。原因是平台把 body 的摘要也混进去了。代码包里sign.py的入口函数签名大致是这样:

def generate_a_bogus(params: str, body: str, user_agent: str, timestamp: int) -> str: """ params: 查询字符串,按 key 排序后拼接,不含 a_bogus body: 请求体原始字符串,GET 请求传空串 user_agent: 当前请求使用的 UA,参与环境指纹计算 timestamp: 毫秒级时间戳,与请求发出时间差不能超过阈值 """

参数顺序不能乱,params必须是排序后的,body必须是原始未编码的字符串。我见过有人把 body 先json.dumps再传进去,结果多了一层转义,签名直接废掉。

2.2 把 JS 里的核心运算翻译成 Python

混淆代码里最核心的是两段:一段是字符串拼接后的位运算,一段是自定义的哈希。位运算部分用的是 32 位整数溢出回绕,Python 的 int 是任意精度,直接算会得到完全不同的结果。所以代码包里所有涉及位运算的地方都加了掩码:

MASK = 0xFFFFFFFF def _rotl(x: int, n: int) -> int: x &= MASK return ((x << n) | (x >> (32 - n))) & MASK def _add(a: int, b: int) -> int: return (a + b) & MASK

_rotl是循环左移,_add是带掩码的加法。这两个函数看着简单,但少了掩码,后面所有中间值都会偏。自定义哈希那块,JS 里用的是类似 DJB2 的变体,初始种子不是常见的 5381,而是一个跟 UA 长度相关的值。代码包里把它抽成了_seed_from_ua,换 UA 的时候种子会变,这也是为什么你复制别人的签名过来用不了——环境指纹对不上。

2.3 本地跑通的最小验证流程

拿到代码包后,别急着接采集框架,先跑单点验证。步骤是这样:

第一步,装依赖。代码包只依赖requests和pycryptodome,没有重依赖:

pip install requests pycryptodome

第二步,改配置。打开config.py,把COOKIE和USER_AGENT换成你自己浏览器里抓到的值。这两个值必须成对出现,用 A 浏览器的 Cookie 配 B 浏览器的 UA,签名能生成但服务端会拒。

第三步,跑验证脚本:

python verify.py --url "https://example.com/aweme/v1/web/aweme/detail/?aweme_id=123" --method GET

脚本会打印生成的a_bogus,然后带着它发一次真实请求,最后输出返回码和响应体前 200 字符。如果返回码是 200 且 body 里有数据,说明链路通了。返回空或者 403,先别改代码,往下看第 4 章的排查清单。

提示:验证阶段建议把请求间隔拉到 3 秒以上,短时间内高频请求会触发另一层风控,那时候你分不清是签名错了还是被限流了。

3. 把签名模块接进采集脚本:参数拼装与请求封装

3.1 查询字符串的拼装规则

签名对params的拼装顺序极其敏感。代码包里build_query函数做的是:按 key 的 ASCII 升序排列,value 做 URL 编码,然后用&连接。注意是编码后再拼接,不是拼接后再编码。这两种做法在 value 含中文或特殊字符时结果完全不同。

from urllib.parse import quote def build_query(params: dict) -> str: items = sorted(params.items(), key=lambda kv: kv[0]) return "&".join(f"{k}={quote(str(v), safe='')}" for k, v in items)

safe=''表示连/都编码。有些接口的 value 里带斜杠,如果不编码,签名对不上。这个细节在文档里不会写,只能靠对比浏览器实际发出的请求发现。

3.2 请求封装与签名注入

单次生成签名不难,难的是把它稳定地接进采集流程。代码包里client.py封装了一个SignedSession,继承自requests.Session,在prepare_request阶段自动注入签名。核心逻辑:

class SignedSession(requests.Session): def prepare_request(self, request): req = super().prepare_request(request) ts = int(time.time() * 1000) query = build_query(dict(parse_qsl(urlparse(req.url).query))) body = req.body.decode() if isinstance(req.body, bytes) else (req.body or "") bogus = generate_a_bogus(query, body, self.headers["User-Agent"], ts) # 把 a_bogus 和 timestamp 追加到 URL sep = "&" if "?" in req.url else "?" req.url = f"{req.url}{sep}a_bogus={bogus}&ts={ts}" return req

这里有个顺序问题:a_bogus和ts是在签名算完之后才追加的,它们本身不参与签名计算。如果你先把a_bogus塞进 params 再算签名,会陷入死循环。ts必须和签名时用的时间戳是同一个值,差一毫秒都可能失败,所以代码里用同一个ts变量。

3.3 并发场景下的注意事项

多线程跑的时候,SignedSession不能全局共享。每个线程要有自己的实例,因为requests.Session内部的连接池和 Cookie 不是线程安全的。代码包里给了一个ThreadLocalSession的写法:

import threading _local = threading.local() def get_session() -> SignedSession: if not hasattr(_local, "session"): _local.session = SignedSession() _local.session.headers.update(HEADERS) return _local.session

每个线程第一次调用get_session时创建自己的 session,后续复用。这样既避免了锁竞争,也保证了 Cookie 隔离。并发数建议控制在 5 以内,再高容易触发频控,而且签名生成本身是 CPU 操作,线程太多反而拖慢。

4. 避坑与排查:签名能生成但请求失败的六种情况

4.1 返回 200 但 body 为空

现象:请求返回码正常,响应体是{}或者{"status_code": 0}但没有数据字段。

原因:签名本身通过了,但ts时间戳和服务器时间偏差超过阈值。平台一般允许 60 秒左右的偏差,但如果你本机时间没同步,或者用了缓存的时间戳,就会命中这个坑。

解决:在生成签名前强制同步一次时间,或者直接用time.time()现取。代码包里verify.py有个--check-clock参数,会打印本机时间和服务器返回头里的时间差,超过 30 秒就先校准系统时间。

4.2 返回 403 且提示签名错误

现象:明确的风控提示,说签名无效。

原因:九成是params拼装顺序错了。常见的是用了requests默认的参数编码,它不保证顺序,而且对空格的处理是+而不是%20。

解决:不要用requests的params参数传查询串,自己用build_query拼好完整 URL 再传。另外检查 value 里有没有中文,有的话确认编码方式是 UTF-8 且做了 percent 编码。

4.3 换台机器就跑不通

现象:本机验证通过,部署到服务器上就失败。

原因:签名逻辑里混入了环境指纹,最常见的是 UA 和屏幕分辨率。代码包里_seed_from_ua只取了 UA,但如果你用的代码版本里还混了别的环境值,换机器就会变。

解决:把config.py里的USER_AGENT固定成和抓包时完全一致的值,不要用fake_useragent随机生成。随机 UA 在签名场景下是自找麻烦。

4.4 跑一段时间后突然全部失败

现象:前几百次请求正常,之后全部返回风控。

原因:触发了频控,跟签名无关。平台的频控维度通常是 IP + Cookie + 时间窗口。

解决:降低并发,加随机延迟,并且定期更换 Cookie。代码包里client.py留了on_rate_limit回调,可以在里面挂代理切换或者暂停逻辑。但注意,签名模块本身不负责代理,那是另一层的事。

4.5 签名长度对但服务端不认

现象:生成的a_bogus长度和浏览器里的一致,格式也对,但服务端就是不认。

原因:中间某个位运算的掩码写错了,导致部分字符正确部分错误。这种最难查,因为肉眼看不出区别。

解决:用代码包里的diff_test.py,它会同时调用浏览器导出的 JS 函数(通过 Node)和 Python 函数,逐字符对比输出。定位到第一个不一致的字符位置,再回去查对应的运算步骤。这个脚本是排查位运算错误最有效的工具。

4.6 Cookie 过期导致的假性签名失败

现象:之前一直正常的脚本,某天开始全部失败,但签名逻辑没动过。

原因:Cookie 里的某个 token 过期了,服务端在签名校验之前就先校验了登录态,返回的错误信息看起来像签名问题。

解决:先单独用requests带 Cookie 发一个不签名的请求,看返回什么。如果提示登录失效,那就是 Cookie 的问题,跟签名无关。代码包里verify.py的--skip-sign参数就是干这个的。

5. 进阶:签名逻辑的版本追踪与自动化回归

签名参数最烦人的地方不是逆向本身,而是平台会不定期更新 JS。今天跑通的代码,下周可能就失效了。所以真正省事的做法不是每次手动重逆,而是建一套版本追踪机制。

我一般会做两件事。第一,把浏览器里导出的核心 JS 函数存成文件,用 Node 跑一个基准测试,生成一组「输入 → 输出」的对照表。第二,Python 侧每次改动后,跑一遍对照表,全部匹配才算通过。代码包里regression/目录就是这个思路:

# regression/run.py import json from sign import generate_a_bogus with open("regression/cases.json") as f: cases = json.load(f) failed = [] for case in cases: got = generate_a_bogus(case["params"], case["body"], case["ua"], case["ts"]) if got != case["expected"]: failed.append({"case": case["name"], "got": got, "want": case["expected"]}) if failed: print(f"{len(failed)} cases failed") for f_ in failed[:3]: print(f_) else: print("all passed")

cases.json里存的是从浏览器抓下来的真实输入输出对,覆盖 GET、POST、带中文参数、带特殊字符等场景。每次平台更新后,重新抓一批 case 加进去,然后改 Python 代码直到全绿。这套流程跑顺之后,更新适配的时间能从半天压缩到半小时以内。

还有一个习惯:把a_bogus的生成逻辑和采集逻辑彻底解耦。签名模块只暴露一个纯函数,不碰网络、不碰文件、不碰全局状态。这样回归测试才能跑得快,也才能在 CI 里跑。我见过太多人把签名和请求揉在一起,结果每次排查都要发真实请求,既慢又容易触发风控。

最后说个验证技巧:如果你不确定某个改动是否影响了签名,不要直接跑采集,先用verify.py发一次单请求,对比改动前后的a_bogus值。值变了不一定错,但值没变而请求失败,那问题一定不在签名上。从那以后我每次改完签名相关代码,都强制先跑一遍回归对照表再碰采集脚本,这个习惯帮我省掉了大量无效排查。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进