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 得到原始请求参数。这套组合拳,有效防止了简单的重放攻击和参数篡改。

所以,这个“深度解析”的目的,绝不是鼓励你去破解、盗用或进行任何违规操作。它的核心价值在于:

  1. 技术学习 :这是一个非常经典的“前端加密,后端解密”的Web安全案例,理解其流程对学习现代Web应用的安全机制大有裨益。
  2. 自动化工具开发 :在合规的前提下(如个人数据备份、学术研究、数据分析),实现合法的自动化操作。例如,备份自己的“我喜欢的音乐”歌单到本地。
  3. 理解协议 :通过逆向工程,可以更深入地理解一个大型商业应用是如何设计其客户端-服务器通信协议的。

本指南将带你完整走一遍从浏览器中定位加密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的代码,直接阅读是灾难。我们需要借助“关键字”进行搜索。

  1. 搜索 encSecKey params :在Sources面板的JS文件中,使用 Ctrl+F 搜索这两个词。你可能会找到它们被赋值的地方,比如 d.encSecKey = c data.params = b 。这里就是加密函数被调用的地方。
  2. 搜索加密算法关键词 :搜索 AES RSA encrypt CryptoJS (网易云早期版本使用过)等。网易云音乐目前使用的是其自定义的加密函数,但内部原理仍是AES和RSA。
  3. 使用“XHR/fetch断点” :在开发者工具的 Sources 面板,右侧有个 XHR/fetch Breakpoints 。点击“+”号,添加一个包含“weapi”的URL断点。然后再次触发请求,代码会自动在发送请求前暂停。这时调用栈(Call Stack)会清晰地展示出从事件触发到最终加密、发送请求的完整函数调用链。顺着调用栈往下找,就能定位到核心的加密函数。

经过一番查找,你最终会定位到一个核心的加密函数。在当前的实现中(请注意,网易云音乐可能会更新),这个函数通常被命名为 window.asrsea 或类似的名字,它接受四个参数。我们可以通过一段简单的代码在控制台验证:

// 在开发者工具的Console中尝试
JSON.stringify(window.asrsea) // 如果存在,会显示函数体(压缩后的)
// 或者直接调用它,传入一些测试数据看看
// 例如:window.asrsea('{"s":"test"}', '010001', '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7', '0CoJUm6Qyw8W8jud')

如果这个函数存在,那么恭喜你,找到了“钥匙”。这四个参数分别是:

  1. 待加密的文本 :通常是JSON格式的请求参数。
  2. RSA公钥的模数(n) :十六进制字符串。
  3. RSA公钥的指数(e) :通常是 010001 (即65537)。
  4. AES的初始密钥(iv) :一个固定的16位字符串。

2.3 核心加密流程拆解

通过分析 window.asrsea 函数(或其内部调用的其他函数),我们可以梳理出标准的加密流程:

  1. 第一次AES加密

    • 生成一个16位的随机字符串作为 secretKey (AES密钥)。
    • 使用 CBC模式 PKCS7填充 方式,用固定的 iv (即传入的第四个参数,如 0CoJUm6Qyw8W8jud )和刚才生成的 secretKey ,对原始的请求文本(JSON字符串)进行AES加密。
    • 将加密结果进行 Base64编码 ,得到中间密文 firstEncrypted
  2. 第二次AES加密

    • 使用同一个固定的 iv ,但这次用另一个 固定的密钥 (实际上就是传入的第四个参数本身, 0CoJUm6Qyw8W8jud ),对 firstEncrypted 这个Base64字符串再次进行AES加密(同样CBC+PKCS7)。
    • 将第二次加密的结果再进行 Base64编码 ,最终得到的就是我们看到的 params 参数。
  3. 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 关键步骤与参数详解

  1. 随机密钥生成 _generate_random_key 函数生成了一个16字节的随机字符串。这个字符串在每次请求时都是不同的,确保了 params 即使对于相同的请求内容也会变化,增加了重放攻击的难度。

  2. 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 )。
  3. RSA加密的核心——反转 :这是最容易出错的一步。在JS代码中,传递给RSA加密函数的明文,是 secretKey 字符串 反转(reverse) 后的结果。在Python中,我们用 plaintext[::-1] 来实现。如果忘记这一步, encSecKey 永远无法被服务器正确解密。

  4. 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 关键请求细节剖析

  1. Content-Type :必须设置为 application/x-www-form-urlencoded 。虽然我们发送的 params encSecKey 看起来像是一长串字符串,但服务器期望以标准表单格式接收它们。 requests data 参数会自动处理字典为这种格式。

  2. 请求头(Headers) User-Agent Referer 是必须的,否则服务器可能拒绝请求或返回非预期结果。 Origin 头对于防止CSRF也有作用,最好加上。

  3. Cookies :对于需要登录态的操作(如获取私人歌单、发送评论),你需要在请求中携带有效的Cookie。你可以通过浏览器登录后,从开发者工具的请求头中复制 Cookie 字段,然后将其添加到 headers 字典中。

    headers['Cookie'] = '你的完整Cookie字符串'
    

    重要警告 :Cookie包含你的登录凭证,请妥善保管,不要泄露给他人或上传到公开仓库。

  4. 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 高级调试技巧

  1. 使用 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代码,你需要重新提取。
  2. 处理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 安全、合规与道德边界

这是最重要的一部分。技术本身无罪,但如何使用技术至关重要。

  1. 严格遵守频率限制 :不要发起高并发、高频次的请求,这会对网易云音乐的服务器造成压力,属于DoS攻击的范畴,也可能导致你的IP被永久封禁。在循环请求中,务必添加随机延时(例如 time.sleep(random.uniform(1, 3)) )。

  2. 尊重版权与用户隐私

    • 仅用于个人学习与数据备份 :本技术解析的主要目的是学习和理解Web安全机制。用于备份自己收藏的音乐、歌单是合理的个人使用。
    • 严禁大规模爬取与分发 :严禁使用此技术大规模爬取歌曲、评论、用户信息等数据用于商业用途或公开分发。这侵犯了网易云音乐和内容创作者的权益。
    • 保护他人隐私 :切勿爬取、存储或传播其他用户的个人信息、私密歌单、评论等。
  3. 关注法律与用户协议 :网易云音乐的用户协议中明确禁止了对其服务的自动化访问、反向工程和数据抓取。你的行为可能违反该协议。因此,所有操作应在最小必要、个人使用的原则下进行,并意识到其中存在的法律风险。

  4. 技术更新的应对 :网易云音乐可能会不定期更新其加密算法或参数。如果某天发现脚本突然失效,请平静地回到第一步——打开浏览器开发者工具,重新进行逆向分析,更新代码中的核心常量。这正是Web逆向工程的常态。

我个人在实际操作中的体会是,逆向工程就像一场与开发者的无声对话。你通过代码去理解他们的设计思路和安全考量。这个过程极大地锻炼了代码阅读、逻辑分析和问题解决能力。但请永远记住,能力越大,责任越大。将这份技术能力用在正当的学习和创造上,才是它最大的价值。最后一个小技巧:在调试加密时,将所有中间变量(明文、密钥、各阶段输出)都打印出来并妥善保存,这比任何日志都管用。

更多推荐