做过网易云爬虫的朋友应该都清楚,网易云音乐评论接口请求时携带的encSecKey、params双层加密堪称入门拦路虎。网上零散教程大多只贴成品加密函数,新手复制完直接一堆报错。今天全程完整拆解评论采集接口的签名逻辑,轻松跑通完整加密流程。
一、抓包分析:锁定评论接口
先随便打开一首音乐,进入评论区可以看到有很多评论,动不动就是几万页的评论。
打开浏览器抓包工具,切换一下评论页,可以看到一个comments/get的请求,这个请求就是获取这首音乐评论的数据接口。

如果没找到这个请求的话,随便复制一条评论搜索一下,就能看到这个接口的请求了。
点击这条请求,右侧面板切换到“载荷”(Payload),可以看到查询字符串和表单数据。

我们主要关注表单里面的encSecKey和params这两个参数。查询字符串里面的csrf_token不需要加密,而表单里面这两个参数是需要JS逆向的,也就是我们本节课的目标。
二、全局搜索:定位加密参数
直接全局搜索encSecKey,你会搜到大量结果,其中很多都是无关的。这时可以改为搜索encSecKey:(加一个冒号),因为给请求主体赋值时,通常是在JSON对象中以key: value的形式书写,冒号是键值对的分隔符,这样能有效过滤干扰项。

虽然加上冒号搜索减少了很多无效干扰项,但现在还是剩下很多encSecKey:这样的赋值。怎么办?一个文件一个文件都打上断点,看哪个断点触发了就去调试那个。
这边优先排在前面的core_3660..js,进入这个JS文件搜索encSecKey:,全部给打上断点。

三、断点调试:找到加密入口
该JS文件的断点打好以后,再去点击切换下一页,这次非常幸运,一发入魂,直接找到给encSecKey赋值的位置了。

你不是很奇怪我是怎么判断就是这个位置的?你可以把鼠标移到W8O变量,看看它的路径,再看看e8e.method、e8e.data,这不就是请求连接、请求方式、请求主体吗?
通过代码可以知道,params和encSecKey的值是bWo4s赋值的,而bWo4s的值又是由window.asrsea函数赋值的:
var bWo4s = window.asrsea(JSON.stringify(i8a), bod3x(["流泪", "强"]), bod3x(AY3x.md), bod3x(["爱心", "女孩", "惊恐", "大笑"]));
e8e.data = j8b.cq8i({
params: bWo4s.encText,
encSecKey: bWo4s.encSecKey
})
在控制台执行asrsea函数,可以发现params和encSecKey的值就是由asrsea生成的。

在控制台中还原asrsea函数的参数,发现除了参数1是动态的值以外,其他的值都是固定的。
四、算法分析:AES + 魔改RSA
继续跟进asrsea函数,看看它的整体加密结构:
function b(a, b) {
var c = CryptoJS.enc.Utf8.parse(b)
, d = CryptoJS.enc.Utf8.parse("0102030405060708")
, e = CryptoJS.enc.Utf8.parse(a)
, f = CryptoJS.AES.encrypt(e, c, {
iv: d,
mode: CryptoJS.mode.CBC
});
return f.toString()
}
function c(a, b, c) {
var d, e;
setMaxDigits(131);
d = new RSAKeyPair(b, "", c);
e = encryptedString(d, a);
return e
}
function d(d, e, f, g) {
var h = {};
var i = a(16);
h.encText = b(d, g);
h.encText = b(h.encText, i);
h.encSecKey = c(i, e, f);
return h
}
代码结构清晰:
b函数:AES-CBC加密,IV固定为0102030405060708c函数:RSA加密,使用setMaxDigits(131)和RSAKeyPaird函数:先AES加密一次,再用随机key做第二次AES加密,最后用RSA加密随机key
本来想用Python直接实现,但发现它的RSA加密流程进行了魔改——包含字符串反转、小端序转换、补0填充、空格分隔输出,和标准RSA完全对不上。

五、代码扣取:无环境检测直接运行
那还是别那么麻烦了,直接扣代码。既然是AES和RSA算法,把全部代码扣下来就行了。这样的代码没有环境检测,不用补环境,最简单。
就从d函数所在的自执行函数开始扣,扣下来写个调用,直接运行缺少哪个函数报错,就去拷贝那个函数进来。
完整扣取代码(保存为 main.js)
window = global;
!function() {
function a(a) {
var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
for (d = 0; a > d; d += 1)
e = Math.random() * b.length,
e = Math.floor(e),
c += b.charAt(e);
return c
}
function b(a, b) {
var c = CryptoJS.enc.Utf8.parse(b)
, d = CryptoJS.enc.Utf8.parse("0102030405060708")
, e = CryptoJS.enc.Utf8.parse(a)
, f = CryptoJS.AES.encrypt(e, c, {
iv: d,
mode: CryptoJS.mode.CBC
});
return f.toString()
}
function c(a, b, c) {
var d, e;
setMaxDigits(131);
d = new RSAKeyPair(b, "", c);
e = encryptedString(d, a);
return e
}
function d(d, e, f, g) {
var h = {}
, i = a(16);
h.encText = b(d, g);
h.encText = b(h.encText, i);
h.encSecKey = c(i, e, f);
return h
}
function e(a, b, d, e) {
var f = {};
return f.encText = c(a + e, b, d),
f
}
window.asrsea = d,
window.ecnonasr = e
}();
// 固定参数
var e = '010001';
var f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7';
var g = '0CoJUm6Qyw8W8jud';
function asrsea(d) {
return window.asrsea(d, e, f, g);
}
// 测试
var d = '{"rid":"R_SO_4_3390243136","offset":"60","total":"false","limit":"20","csrf_token":""}';
var cc = asrsea(d);
console.log("params=" + cc.encText + "&encSecKey=" + cc.encSecKey);
六、Python调用:完整流程串联
代码都扣下来以后,就可以写个Python程序调用asrsea函数生成encSecKey和params,发送请求试试看可不可以正常获取到评论列表。
其中参数d里的3390243136是歌曲ID,爬取不同的歌曲需要更换。
import json
import execjs
import requests
ctx = None
def init_js(js_file_path="main.js"):
global ctx
with open(js_file_path, 'r', encoding='utf-8') as f:
js_code = f.read()
ctx = execjs.compile(js_code)
def get_comments(item_id):
url = f'https://业务网址/weapi/comment/resource/comments/get?csrf_token=7a1b2cc8c9c64af957a742f65dac4dfe'
headers = {
"accept": "*/*",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://业务网址",
"referer": f"https://业务网址/playlist?id={item_id}",
"user-agent": "Mozilla/5.0"
}
encrypt_data = {
"rid": f"R_SO_4_{item_id}",
"threadId": f"R_SO_4_{item_id}",
"pageNo": "1",
"pageSize": "20",
"cursor": "-1",
"offset": "0",
"orderType": "1",
"csrf_token": "7a1b2cc8c9c64af957a742f65dac4dfe"
}
d = json.dumps(encrypt_data, separators=(',', ':'))
result = ctx.call("asrsea", d)
post_data = {
'params': result['encText'],
'encSecKey': result['encSecKey']
}
response = requests.post(url, headers=headers, data=post_data)
all_data = response.json()
all_comments = all_data.get('data', {}).get('comments', [])
return all_comments
if __name__ == '__main__':
init_js()
id = '3390243136'
comments = get_comments(id)
print("n" + "=" * 60)
print(f"评论列表 (共{len(comments)}条)")
print("=" * 60)
for i, c in enumerate(comments, 1):
print(f"{c['user']['nickname']}t{c['content'][:20]}t {c.get('likedCount', 0)}人点赞")
print("n" + "=" * 60)
