sign 参数:看着像 md5,确实是 md5(的变种)
QQ 音乐 web 端 u.y.qq.com 的 CGI 接口带一个 sign 参数,32 位 hex。和别家把签名算法藏在 VMP 里不同,QQ 音乐的签名就是魔改 md5 + 字符置换,混淆程度中等,是练"从 webpack chunk 里抠函数"的好目标。
定位:搜对关键词事半功倍
QQ 音乐前端是 webpack 打包,直接搜 sign 会被淹没。正确的搜索词是功能命名:
"securitySign" ← 直接命中签名函数
"getSign" ← 部分版本命中后能看到签名函数在一个独立 chunk 里,入参是请求体对象,返回值就是 sign。
算法拆解
签名逻辑拆开三步:
- 拼接:固定前缀 salt + 请求体的 JSON 字符串。salt 形如
"CJBPACrRuNy7"(这是历史上公开过的版本,当前版本以你抓的为准——2024 年后改成了动态下发) - md5:对拼接串算 md5,转大写 hex
- 字符置换:对 hex 串做奇偶位互换类的前置变换后输出
// 抠出来的最小可运行单元(结构示意)
function getSecuritySign(reqData) {
var salt = window.__SIGN_SALT__ || "CJBPACrRuNy7"; // 动态 salt 从页面配置取
var raw = salt + JSON.stringify(reqData);
var hash = md5(raw).toUpperCase();
// 置换:相邻字符互换
var out = '';
for (var i = 0; i < hash.length; i += 2) {
out += (hash[i+1] || '') + hash[i];
}
return out;
}动态 salt:版本演进的关键
早期版本 salt 硬编码在 JS 里,被公开后腾讯改成了页面配置下发:salt 藏在首页 HTML 或某个配置接口的返回里。工程上的应对:
import re, requests
def get_sign_salt(session):
html = session.get('https://y.qq.com/').text
# salt 在页面内嵌配置里,正则特征以实际页面为准
m = re.search(r'"signSalt"\s*:\s*"([A-Za-z0-9]+)"', html)
return m.group(1) if m else None把 salt 做成启动时提取 + 定时刷新,不要写死。写死的下场是腾讯换 salt 那天你的服务集体 401。
接口体系的另一半:comm 公共参数
sign 之外,QQ 音乐接口还有一套 comm 公共参数体系:
{
"comm": {
"uin": "QQ号",
"guid": "10位随机数",
"qqmusic_key": "登录态key",
"ct": 24, "cv": 80800, "format": "json"
},
"req_0": { "module": "...", "method": "...", "param": {...} }
}guid:设备标识,10 位数字,协议模拟时固定一个qqmusic_key:登录态令牌,扫码登录后下发- 业务参数包在
req_0(可多个,req_1、req_2...)里,sign 签的是整个请求体,包括 comm——所以改 comm 也要重签
精髓:三个坑
- JSON.stringify 的 key 顺序。sign 签的是
JSON.stringify(reqData)的结果,JS 的对象 key 顺序是插入序,Python 复现时 dict 构造顺序必须和浏览器一致。对不上时先把两边的待签字符串打印出来 diff。 - vkey(歌曲播放地址)是独立体系。获取播放 URL 的
musicu.fcg接口除了 sign 还要qqmusic_key,且返回的 vkey 有有效期(约 2 小时),别缓存播放地址。 - 签名错误返回
param error而非明确提示。调试基准:先用"搜索"这种公开接口验证签名正确性,通了再接登录态接口。
总结
- sign = 置换(md5(salt + body JSON)),算法本身半小时能搞定
- salt 已动态化,启动时从页面提取,别写死
- comm 公共参数参与签名,guid 固定、key 顺序保序
- 抠代码优先于手搓:整个 chunk 连带 md5 依赖一起抠,对拍后上线