网易云音乐API加密参数逆向解析与Python复现指南
1. 项目概述:为什么我们需要破解网易云音乐的加密参数?
如果你曾经尝试过自己写个爬虫或者脚本,去抓取网易云音乐的歌单、评论或者下载歌曲,那你大概率会在第一个请求上就碰壁。你会发现,那些看似普通的搜索、播放列表请求,其核心参数 params 和 encSecKey 是一长串完全看不懂的密文。这堵墙,就是网易云音乐为了保护其API接口和数据安全而设置的加密防线。
我最初接触这个需求,是想做一个个人用的歌单备份工具。直接用浏览器开发者工具看到的请求,复制到Python的requests里一跑,返回的不是心心念念的JSON数据,而是一个冷冰冰的“参数错误”。那一刻我就明白,事情没那么简单。网易云音乐的Web端(music.163.com)对其核心API请求进行了非对称加密,尤其是涉及用户数据、歌曲详情、评论等敏感或核心功能时, params 和 encSecKey 这两个参数是绕不开的坎。
简单来说,这个过程是这样的:前端JavaScript会将你的请求参数(比如搜索关键词“周杰伦”、歌单ID等)先进行AES加密,生成密文作为 params ;然后再用RSA公钥加密AES的密钥,生成 encSecKey 。服务器端用对应的RSA私钥解密 encSecKey 得到AES密钥,再用这个AES密钥解密 params 得到原始请求参数。这套组合拳,有效防止了简单的重放攻击和参数篡改。
所以,这个“深度解析”的目的,绝不是鼓励你去破解、盗用或进行任何违规操作。它的核心价值在于:
- 技术学习 :这是一个非常经典的“前端加密,后端解密”的Web安全案例,理解其流程对学习现代Web应用的安全机制大有裨益。
- 自动化工具开发 :在合规的前提下(如个人数据备份、学术研究、数据分析),实现合法的自动化操作。例如,备份自己的“我喜欢的音乐”歌单到本地。
- 理解协议 :通过逆向工程,可以更深入地理解一个大型商业应用是如何设计其客户端-服务器通信协议的。
本指南将带你完整走一遍从浏览器中定位加密JS代码,到分析其加密逻辑,最后在Python中完整复现这一流程的全过程。你需要具备基本的JavaScript阅读能力和Python编程基础。我们会用到一些常见的工具,如浏览器开发者工具、Python的 Crypto / cryptography 库等。
2. 加密逻辑逆向:在浏览器中寻找“钥匙”
一切始于浏览器。我们的目标是找到生成 params 和 encSecKey 的那段JavaScript代码,并理解它。
2.1 定位加密函数入口
打开网易云音乐网页版(music.163.com),按F12打开开发者工具,切换到 Network(网络) 面板。先清空记录,然后进行一个会触发加密API的操作,比如在搜索框输入一个词并回车。
在纷繁的网络请求中,找到类型为 XHR 或 Fetch 的请求,其请求URL通常包含 weapi 字样,例如 /weapi/cloudsearch/get/web (搜索接口)或 /weapi/v6/playlist/detail (歌单详情)。点击这个请求,在 Headers 选项卡的底部,找到 Form Data 或 Payload ,你就能看到 params 和 encSecKey 这两个字段,以及可能还有 csrf_token 。
关键的一步来了:在 Initiator 列(或右键请求,选择“Reveal in Sources panel”),点击调用栈中最顶层的那个JS文件链接。这通常会带你进入一个被压缩混淆过的JavaScript文件(比如 core.js , index.js 等)。
2.2 在混淆代码中“大海捞针”
面对压缩成一两行、变量名都是a,b,c,d的代码,直接阅读是灾难。我们需要借助“关键字”进行搜索。
- 搜索
encSecKey或params:在Sources面板的JS文件中,使用Ctrl+F搜索这两个词。你可能会找到它们被赋值的地方,比如d.encSecKey = c或data.params = b。这里就是加密函数被调用的地方。 - 搜索加密算法关键词 :搜索
AES,RSA,encrypt,CryptoJS(网易云早期版本使用过)等。网易云音乐目前使用的是其自定义的加密函数,但内部原理仍是AES和RSA。 - 使用“XHR/fetch断点” :在开发者工具的 Sources 面板,右侧有个 XHR/fetch Breakpoints 。点击“+”号,添加一个包含“weapi”的URL断点。然后再次触发请求,代码会自动在发送请求前暂停。这时调用栈(Call Stack)会清晰地展示出从事件触发到最终加密、发送请求的完整函数调用链。顺着调用栈往下找,就能定位到核心的加密函数。
经过一番查找,你最终会定位到一个核心的加密函数。在当前的实现中(请注意,网易云音乐可能会更新),这个函数通常被命名为 window.asrsea 或类似的名字,它接受四个参数。我们可以通过一段简单的代码在控制台验证:
// 在开发者工具的Console中尝试
JSON.stringify(window.asrsea) // 如果存在,会显示函数体(压缩后的)
// 或者直接调用它,传入一些测试数据看看
// 例如:window.asrsea('{"s":"test"}', '010001', '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7', '0CoJUm6Qyw8W8jud')
如果这个函数存在,那么恭喜你,找到了“钥匙”。这四个参数分别是:
- 待加密的文本 :通常是JSON格式的请求参数。
- RSA公钥的模数(n) :十六进制字符串。
- RSA公钥的指数(e) :通常是
010001(即65537)。 - AES的初始密钥(iv) :一个固定的16位字符串。
2.3 核心加密流程拆解
通过分析 window.asrsea 函数(或其内部调用的其他函数),我们可以梳理出标准的加密流程:
-
第一次AES加密 :
- 生成一个16位的随机字符串作为
secretKey(AES密钥)。 - 使用 CBC模式 、 PKCS7填充 方式,用固定的
iv(即传入的第四个参数,如0CoJUm6Qyw8W8jud)和刚才生成的secretKey,对原始的请求文本(JSON字符串)进行AES加密。 - 将加密结果进行 Base64编码 ,得到中间密文
firstEncrypted。
- 生成一个16位的随机字符串作为
-
第二次AES加密 :
- 使用同一个固定的
iv,但这次用另一个 固定的密钥 (实际上就是传入的第四个参数本身,0CoJUm6Qyw8W8jud),对firstEncrypted这个Base64字符串再次进行AES加密(同样CBC+PKCS7)。 - 将第二次加密的结果再进行 Base64编码 ,最终得到的就是我们看到的
params参数。
- 使用同一个固定的
-
RSA加密生成encSecKey :
- 将第一步随机生成的
secretKey(16位字符串)进行 反转 (reverse)。 - 将这个反转后的字符串,用 RSA公钥 进行加密。公钥由模数
n(第二个参数)和指数e(第三个参数)构成。加密时,会先将字符串转换成大整数,然后计算(明文 ^ e) mod n。 - 将RSA加密后得到的大整数,转换成 16进制字符串 。这个字符串就是
encSecKey。
- 将第一步随机生成的
注意 :以上流程是基于一个较长稳定期的实现总结的。网易云音乐可能会调整细节,例如AES的
iv、固定的密钥、甚至加密顺序。因此,最可靠的方法永远是通过你当前时间点逆向得到的代码为准。但整体思路(两次AES + RSA)是稳定的。
3. Python复现:构建我们自己的加密引擎
理解了原理,我们就可以在Python中复现这个流程了。我们将使用 pycryptodome 库来处理AES和RSA加密。
3.1 环境准备与依赖安装
首先,确保你的Python环境(建议3.6以上)并安装必要的库:
pip install pycryptodome requests
pycryptodome:这是Crypto库的一个活跃分支,提供了强大的加密算法支持。requests:用于最终发送HTTP请求。
3.2 核心加密函数实现
下面是一个完整的Python类,它封装了上述加密逻辑。你需要将从JS代码中逆向出来的关键常量替换到对应位置。
import base64
import json
import random
import string
from Crypto.Cipher import AES, PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto.Util.Padding import pad, unpad
import binascii
class NeteaseMusicEncrypt:
"""
网易云音乐Web端API参数加密器
核心参数需要从当前网页的JS代码中逆向获取。
"""
def __init__(self):
# !!!这些常量必须从目标网站的JS代码中实时获取 !!!
# RSA公钥指数,通常是'010001'
self.e = '010001'
# RSA公钥模数 (n),一个很长的16进制字符串
self.n = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
# 第一次AES加密的固定密钥和IV,通常是同一个16字节字符串
self.aes_key = b'0CoJUm6Qyw8W8jud' # 注意是bytes类型
self.aes_iv = b'0102030405060708' # 常见的固定IV,也可能是其他值,需确认
# 用于生成随机AES密钥的字符集
self.charset = string.ascii_letters + string.digits
def _generate_random_key(self, length=16):
"""生成指定长度的随机字符串,用作第一次AES加密的密钥"""
return ''.join(random.choice(self.charset) for _ in range(length)).encode('utf-8')
def _aes_encrypt(self, plaintext, key, iv):
"""AES加密,CBC模式,PKCS7填充,输出Base64"""
# 确保明文是bytes
if isinstance(plaintext, str):
plaintext = plaintext.encode('utf-8')
# PKCS7填充
padded_data = pad(plaintext, AES.block_size, style='pkcs7')
# 创建加密器
cipher = AES.new(key, AES.MODE_CBC, iv)
# 加密
ciphertext = cipher.encrypt(padded_data)
# Base64编码
return base64.b64encode(ciphertext).decode('utf-8')
def _rsa_encrypt(self, plaintext):
"""RSA加密,使用固定的公钥(n, e)"""
# 明文反转(关键步骤!)
plaintext = plaintext[::-1]
# 将明文转换为大整数
plaintext_int = int.from_bytes(plaintext.encode('utf-8'), byteorder='big')
n_int = int(self.n, 16)
e_int = int(self.e, 16)
# 进行RSA计算:cipher_int = (plaintext_int ^ e_int) mod n_int
# 注意:这里使用了pow函数的三个参数形式进行模幂运算,效率远高于 (plaintext_int ** e_int) % n_int
cipher_int = pow(plaintext_int, e_int, n_int)
# 将结果整数转换为16进制字符串,并确保长度为256字符(补零)
cipher_hex = format(cipher_int, 'x')
# RSA加密后长度固定,需要补前导零到512位(256个十六进制字符)
return cipher_hex.zfill(256)
def encrypt(self, data):
"""
主加密函数,模拟window.asrsea。
参数data: 字典类型,即要发送的请求参数。
返回: 包含params和encSecKey的字典。
"""
# 1. 将请求参数转换为JSON字符串
text = json.dumps(data)
# 2. 生成随机AES密钥 (secretKey)
secret_key = self._generate_random_key(16)
# 3. 第一次AES加密:用secretKey加密文本
first_encrypted = self._aes_encrypt(text, secret_key, self.aes_iv)
# 4. 第二次AES加密:用固定key加密第一次的结果
params = self._aes_encrypt(first_encrypted, self.aes_key, self.aes_iv)
# 5. RSA加密:加密secretKey(注意要反转)
enc_seckey = self._rsa_encrypt(secret_key.decode('utf-8'))
return {
'params': params,
'encSecKey': enc_seckey
}
# 使用示例
if __name__ == '__main__':
encryptor = NeteaseMusicEncrypt()
# 构造一个搜索“周杰伦”的请求参数
search_data = {
's': '周杰伦',
'type': 1, # 1代表单曲
'offset': 0,
'total': True,
'limit': 20
}
encrypted_params = encryptor.encrypt(search_data)
print("加密后的参数:")
print(f"params: {encrypted_params['params']}")
print(f"encSecKey: {encrypted_params['encSecKey']}")
3.3 关键步骤与参数详解
-
随机密钥生成 :
_generate_random_key函数生成了一个16字节的随机字符串。这个字符串在每次请求时都是不同的,确保了params即使对于相同的请求内容也会变化,增加了重放攻击的难度。 -
AES加密细节 :
- 模式与填充 :必须使用 CBC模式 和 PKCS7填充 。
pycryptodome的pad函数可以方便地实现PKCS7填充。 - 密钥与IV :第一次加密的密钥是随机的
secret_key,第二次加密的密钥是固定的self.aes_key。而 IV(初始化向量)在两次加密中都是固定的self.aes_iv。这个IV值必须和JS中使用的一致,常见的值是b'0102030405060708',但务必通过逆向确认。 - Base64编码 :AES加密输出的是字节,需要转换为Base64字符串。注意编码解码的一致性(
utf-8)。
- 模式与填充 :必须使用 CBC模式 和 PKCS7填充 。
-
RSA加密的核心——反转 :这是最容易出错的一步。在JS代码中,传递给RSA加密函数的明文,是
secretKey字符串 反转(reverse) 后的结果。在Python中,我们用plaintext[::-1]来实现。如果忘记这一步,encSecKey永远无法被服务器正确解密。 -
RSA计算与输出格式 :
- 模幂运算 :使用
pow(plaintext_int, e_int, n_int)来计算(明文^e) mod n,这是RSA加密的核心。直接计算明文^e会得到一个天文数字,效率极低且可能溢出,pow的三参数形式进行了优化。 - 十六进制与补零 :计算结果需要转换为16进制字符串。由于RSA加密后的密文长度是固定的(由模数
n的位数决定),而Python的format(cipher_int, 'x')可能会省略前导零。服务器期望一个固定长度的encSecKey(通常是256个十六进制字符,即1024位RSA密钥)。因此,必须用zfill(256)在左侧补零到指定长度。
- 模幂运算 :使用
实操心得 :在调试过程中,最有效的验证方法不是直接发请求,而是 与浏览器行为对比 。在浏览器中执行一次操作,在Console里打印出加密前的明文、随机生成的
secretKey,以及最终的params和encSecKey。然后在Python脚本中,使用 完全相同的明文和手动指定的secretKey(而不是随机生成),运行加密函数。对比两者的输出是否完全一致。只有这样才能隔离随机性带来的干扰,确认加密逻辑100%正确。
4. 发起请求与实战应用
加密问题解决后,发起请求就变得简单了。但还有一些细节需要注意。
4.1 构建完整的请求
我们使用 requests 库来发送POST请求。网易云音乐的API端点通常是 https://music.163.com/weapi/xxx 。
import requests
def search_song(keyword, limit=20):
"""
搜索歌曲示例
"""
# 1. 初始化加密器
encryptor = NeteaseMusicEncrypt() # 使用上面定义的类
# 2. 构造请求参数(明文)
raw_data = {
's': keyword,
'type': 1, # 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户
'offset': 0,
'total': True,
'limit': limit
}
# 3. 加密参数
encrypted_data = encryptor.encrypt(raw_data)
# 4. 设置请求头,模拟浏览器
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Referer': 'https://music.163.com/',
'Content-Type': 'application/x-www-form-urlencoded', # 注意这个Content-Type
'Origin': 'https://music.163.com'
}
# 5. 发送POST请求
# 注意:加密后的参数需要作为表单数据发送
url = 'https://music.163.com/weapi/cloudsearch/get/web'
response = requests.post(url, data=encrypted_data, headers=headers)
# 6. 处理响应
if response.status_code == 200:
result = response.json()
# 检查返回码,200表示成功
if result.get('code') == 200:
songs = result.get('result', {}).get('songs', [])
for song in songs:
print(f"歌曲: {song['name']} - {', '.join([ar['name'] for ar in song['ar']])}")
else:
print(f"API返回错误: {result.get('code')}, 消息: {result.get('msg')}")
else:
print(f"网络请求失败: {response.status_code}")
return response.json()
# 调用示例
if __name__ == '__main__':
search_song('周杰伦')
4.2 关键请求细节剖析
-
Content-Type :必须设置为
application/x-www-form-urlencoded。虽然我们发送的params和encSecKey看起来像是一长串字符串,但服务器期望以标准表单格式接收它们。requests的data参数会自动处理字典为这种格式。 -
请求头(Headers) :
User-Agent和Referer是必须的,否则服务器可能拒绝请求或返回非预期结果。Origin头对于防止CSRF也有作用,最好加上。 -
Cookies :对于需要登录态的操作(如获取私人歌单、发送评论),你需要在请求中携带有效的Cookie。你可以通过浏览器登录后,从开发者工具的请求头中复制
Cookie字段,然后将其添加到headers字典中。headers['Cookie'] = '你的完整Cookie字符串'重要警告 :Cookie包含你的登录凭证,请妥善保管,不要泄露给他人或上传到公开仓库。
-
csrf_token :有些接口(特别是涉及写操作的,如点赞)还需要一个
csrf_token参数。这个token通常可以在页面的HTML中或者Cookie里找到(名为__csrf)。你需要将其提取出来,并添加到raw_data明文参数中一起加密。
4.3 更多接口示例
掌握了搜索,其他接口大同小异,主要区别在于 请求的URL 和 明文的参数结构 。
获取歌单详情:
def get_playlist_detail(playlist_id):
encryptor = NeteaseMusicEncrypt()
raw_data = {
'id': playlist_id,
'n': 1000, # 获取的歌单详情数量
'offset': 0
}
encrypted_data = encryptor.encrypt(raw_data)
headers = { ... } # 同上
url = 'https://music.163.com/weapi/v6/playlist/detail' # 注意接口地址
response = requests.post(url, data=encrypted_data, headers=headers)
# ... 处理响应
获取歌曲详情/播放链接:
def get_song_detail(song_ids): # song_ids 是一个列表,如 [123456, 789012]
encryptor = NeteaseMusicEncrypt()
# 注意参数c是一个JSON字符串的字符串
c = json.dumps([{'id': str(id)} for id in song_ids])
raw_data = {
'c': c,
'ids': json.dumps(song_ids)
}
encrypted_data = encryptor.encrypt(raw_data)
url = 'https://music.163.com/weapi/v3/song/detail'
# ... 发送请求和处理
注意事项 :不同接口的参数结构差异很大。最准确的方法是,在浏览器中操作一次,然后在 Network 面板查看该请求的 Payload (请求负载),直接复制其原始数据(通常是JSON格式),这就是你需要构造的
raw_data。我们的加密器只是原封不动地将这个JSON字符串加密而已。
5. 常见问题、调试技巧与安全边界
在实际操作中,你肯定会遇到各种问题。这里记录了一些常见的坑和解决方法。
5.1 常见错误码与排查表
| 错误码/现象 | 可能原因 | 排查步骤 |
|---|---|---|
-460 |
最常见的错误, 加密校验失败 。 | 1. 核对核心常量 : n , e , aes_key , aes_iv 是否与当前网页JS中的完全一致? 2. 检查RSA反转 :确认在RSA加密前,是否对 secretKey 进行了 [::-1] 反转操作。 3. 检查编码 :所有字符串到字节的转换是否都用了 utf-8 ?Base64编码是否正确? 4. 验证单个步骤 :在浏览器控制台执行加密,记录下 secretKey 、中间密文,与Python每一步的输出对比。 |
-2 |
参数错误,通常是明文参数结构不对。 | 1. 对比浏览器中抓包的 Request Payload ,确保你的 raw_data 字典的键、值、格式(特别是嵌套的JSON字符串)完全一致。 2. 检查接口URL是否正确。 |
401 |
需要登录,但请求未携带有效Cookie。 | 1. 为请求头添加 Cookie 。 2. 确认Cookie未过期。对于需要登录的接口,这是必须的。 |
200 但数据为空 |
请求成功,但可能被风控或参数有细微错误。 | 1. 检查 User-Agent 和 Referer 等请求头是否模拟到位。 2. 尝试降低请求频率,加入随机延时。 3. 某些接口对未登录用户返回的数据有限。 |
| Python加密结果与JS不一致 | 加密逻辑或参数有误。 | 黄金调试法 :在JS端固定 secretKey (修改JS代码或找到生成它的函数,让它返回一个固定值),然后分别用JS和Python对 相同的明文 和 相同的固定secretKey 进行加密,逐环节(第一次AES结果、第二次AES结果、RSA输入明文、RSA输出)对比输出。 |
5.2 高级调试技巧
-
使用
execjs直接调用JS函数 :如果Python复现遇到难以解决的编码或算法细节问题,一个取巧但稳定的方法是使用execjs库,直接在Python中执行从网页提取出来的加密JS代码。import execjs # 将从网页中提取的加密函数JS代码保存为字符串 js_code = """ function encrypt(text) { // 这里是 window.asrsea 或相关函数的完整代码 return {params: ..., encSecKey: ...}; } """ ctx = execjs.compile(js_code) result = ctx.call('encrypt', json.dumps(raw_data)) # result 就是包含params和encSecKey的字典- 优点 :100%准确,无需关心JS内部的实现细节。
- 缺点 :依赖Node.js环境(
execjs的后端),性能稍差,且如果网易云音乐更新JS代码,你需要重新提取。
-
处理Cookie与会话 :对于需要连续多个请求的操作,使用
requests.Session()来保持Cookie和连接池,更高效。session = requests.Session() session.headers.update({...}) # 设置公共请求头 session.cookies.update({'__csrf': 'your_csrf_token', ...}) # 设置Cookie response = session.post(url, data=encrypted_data)
5.3 安全、合规与道德边界
这是最重要的一部分。技术本身无罪,但如何使用技术至关重要。
-
严格遵守频率限制 :不要发起高并发、高频次的请求,这会对网易云音乐的服务器造成压力,属于DoS攻击的范畴,也可能导致你的IP被永久封禁。在循环请求中,务必添加随机延时(例如
time.sleep(random.uniform(1, 3)))。 -
尊重版权与用户隐私 :
- 仅用于个人学习与数据备份 :本技术解析的主要目的是学习和理解Web安全机制。用于备份自己收藏的音乐、歌单是合理的个人使用。
- 严禁大规模爬取与分发 :严禁使用此技术大规模爬取歌曲、评论、用户信息等数据用于商业用途或公开分发。这侵犯了网易云音乐和内容创作者的权益。
- 保护他人隐私 :切勿爬取、存储或传播其他用户的个人信息、私密歌单、评论等。
-
关注法律与用户协议 :网易云音乐的用户协议中明确禁止了对其服务的自动化访问、反向工程和数据抓取。你的行为可能违反该协议。因此,所有操作应在最小必要、个人使用的原则下进行,并意识到其中存在的法律风险。
-
技术更新的应对 :网易云音乐可能会不定期更新其加密算法或参数。如果某天发现脚本突然失效,请平静地回到第一步——打开浏览器开发者工具,重新进行逆向分析,更新代码中的核心常量。这正是Web逆向工程的常态。
我个人在实际操作中的体会是,逆向工程就像一场与开发者的无声对话。你通过代码去理解他们的设计思路和安全考量。这个过程极大地锻炼了代码阅读、逻辑分析和问题解决能力。但请永远记住,能力越大,责任越大。将这份技术能力用在正当的学习和创造上,才是它最大的价值。最后一个小技巧:在调试加密时,将所有中间变量(明文、密钥、各阶段输出)都打印出来并妥善保存,这比任何日志都管用。
更多推荐
所有评论(0)