网易云音乐API逆向实战:AES+RSA混合加密参数破解与Python实现
1. 项目概述与背景最近在搞一个音乐数据分析的小项目需要批量获取网易云音乐上的歌曲信息、评论和歌单数据。一开始想着直接用官方API结果发现官方接口要么不开放要么限制得死死的。于是很自然地我把目光投向了网页端和移动端App。打开浏览器开发者工具随便搜索一首歌你会发现一个叫weapi的接口它的请求体里有两个非常关键的加密参数params和encSecKey。这两个参数就像是打开数据大门的钥匙每次请求都必须携带而且每次都不一样。这就是典型的客户端加密服务端解密的玩法目的就是为了防止我们这些“爬虫工程师”轻易地拿到数据。这个逆向过程本质上就是一场与前端工程师的“攻防战”。他们用JavaScript把核心的加密逻辑写在网页里我们则需要从这堆混淆、压缩过的代码中找到生成这两个参数的算法然后用Python或者其他语言复现出来。这不仅仅是复制粘贴代码更需要理解其加密流程、密钥管理和数据填充的机制。对于前端逆向新手来说网易云音乐是一个非常好的练手对象它的加密逻辑清晰典型涉及AES和RSA两种主流算法而且社区资料丰富踩过的坑基本都能找到答案。通过这个实战你不仅能学会如何爬取网易云的数据更能掌握一套通用的JS逆向分析思路以后遇到淘宝、抖音、知乎的加密参数你也能从容应对。2. 核心加密逻辑深度拆解要逆向params和encSecKey我们必须先搞清楚它们是怎么来的。通过分析网易云音乐网页端的核心JavaScript文件通常是core.js或经过混淆的index.xxxxx.js我们可以梳理出完整的加密链条。2.1 加密流程全景图整个加密过程可以概括为“两次加密一次包装”。它不是简单地对请求参数进行一次加密而是一个精心设计的、包含对称与非对称加密的混合体系。第一次加密AES对称加密首先将我们原始的请求参数一个JSON字符串比如{ids:[123456],csrf_token:}进行AES加密。这里使用的模式是CBC填充方式是PKCS7。AES加密需要一个密钥key和一个初始向量iv。在网易云的实现中这两个值都是固定的16位字符串。这一步生成了密文A。第二次加密RSA非对称加密然后将上一步AES加密所用的key本身使用RSA算法进行加密。RSA加密需要公钥。网易云使用了一个固定的、非常长的RSA公钥。这一步生成了密文B。参数组装最后将密文A经过AES加密的请求参数进行Base64编码作为最终的params参数。将密文B经过RSA加密的AES密钥进行Base64编码作为最终的encSecKey参数。这两个Base64字符串连同其他一些固定或易得的请求头如csrf_token有时可为空一起发送给服务器。服务器端拿到params和encSecKey后会用自己的RSA私钥解密encSecKey得到AES的key再用这个key和固定的iv去解密params最终得到我们原始的请求JSON。注意这里有一个非常关键的细节网易云音乐在2020年左右进行过一次重要的加密升级。在此之前RSA加密用的是比较标准的PKCS1填充模式。但在升级后它改用了一种叫做**“随机填充”**的模式。如果你在网上找到的旧代码突然不能用了十有八九是卡在了这里。新的RSA加密函数在JavaScript中通常表现为一个接收三个参数的函数(a, b, c)其中a是待加密文本AES keyb是公钥指数010001c是公钥模数那串很长的16进制数。这个函数内部实现了对明文进行随机填充后再进行RSA运算的过程。2.2 关键常量与函数定位在混淆的JS代码中找到这些常量和方法是逆向的第一步。它们通常以全局变量或某个大对象属性的形式存在。AES密钥key和初始向量iv搜索诸如0CoJUm6Qyw8W8jud、0102030405060708这样的字符串。前者是经典的AESkey后者是经典的AESiv。它们通常是硬编码在代码里的。RSA公钥搜索一长串以00开头、长度超过256位的十六进制字符串。这就是RSA的公钥模数n。公钥指数e通常是010001即65537。核心加密函数在Chrome开发者工具的“Sources”面板中对疑似加密函数的调用处比如网络请求的initiator栈打上断点然后触发一次搜索或播放操作。程序会在断点处暂停这时你可以一步步跟进去Step into找到最终执行加密的那个函数。这个函数可能被命名为window.asrsea、d、encrypt或其它混淆后的名字。一个实用的技巧是在开发者工具的“Console”中尝试直接调用你找到的加密函数并传入测试数据看输出是否和网络请求中的params、encSecKey一致。这是验证你是否找对函数的最直接方法。3. 逆向分析与Python复现全流程理解了原理接下来就是动手环节。我们将使用Python的PyExecJS库来执行JavaScript代码这是逆向初期最稳妥、最接近原生态的方法。3.1 环境准备与依赖安装首先确保你的Python环境已经就绪。我们需要两个核心库pip install pyexecjs requestspyexecjs: 用于在Python中执行JavaScript代码。它需要一个JS运行时环境在Windows上通常会自动使用系统自带的JScriptIE引擎但更推荐安装Node.js因为其性能和支持的ES特性更好。在Mac/Linux上它默认会寻找Node.js。requests: 用于发送最终的HTTP请求。实操心得PyExecJS虽然方便但在Windows上使用JScript引擎时对现代JavaScriptES6的支持很差经常会报语法错误。强烈建议所有平台都安装Node.js。安装后PyExecJS会自动优先使用Node.js作为运行时兼容性和性能都会大幅提升。你可以通过execjs.get().name来检查当前使用的运行时。3.2 提取并净化JavaScript加密代码我们不能直接把整个core.js可能有几兆扔给PyExecJS执行那样效率低且容易出错。我们需要从中提取出最核心的加密函数及其依赖。定位函数在开发者工具中通过断点调试找到生成params和encSecKey的最终函数。假设我们找到的函数叫window.encrypt。查看函数定义在Console中输入window.encrypt.toString()将这个函数的完整代码包括其内部定义的所有子函数复制出来。构建独立JS环境新建一个encrypt.js文件。将复制出来的函数代码粘贴进去。然后我们需要手动补全这个函数所依赖的外部变量或函数。如果window.encrypt内部使用了另一个全局函数A你同样需要找到A.toString()并复制进来。将那些硬编码的常量key,iv, RSA公钥也作为变量定义在文件里。创建对外接口在encrypt.js文件末尾添加一个导出函数方便Python调用。例如// ... 之前的加密函数和常量定义 ... function get_encrypted_params(text) { // 假设原始的window.encrypt接收一个明文JSON字符串返回一个包含params和encSecKey的对象 var result window.encrypt(text); return { params: result.encText, // 对应params encSecKey: result.encSecKey // 对应encSecKey }; }3.3 Python调用与请求封装现在我们可以在Python中加载这个JS文件并调用加密函数了。import execjs import requests import json class NeteaseMusicEncryptor: def __init__(self, js_file_pathencrypt.js): 初始化加载JS加密代码。 :param js_file_path: 包含加密函数的JS文件路径 with open(js_file_path, r, encodingutf-8) as f: js_code f.read() self.ctx execjs.compile(js_code) # 编译JS代码 def encrypt_params(self, raw_data): 加密原始请求数据。 :param raw_data: 字典格式的原始请求参数如 {ids: [123456], csrf_token: } :return: 包含加密后 params 和 encSecKey 的字典 # 将字典转换为JSON字符串 text json.dumps(raw_data, separators(,, :), ensure_asciiFalse) # 调用JS函数进行加密 encrypted self.ctx.call(get_encrypted_params, text) return encrypted def search_song(keyword): 示例搜索歌曲 encryptor NeteaseMusicEncryptor() # 1. 构造原始请求参数需要根据实际API分析 # 网易云搜索接口的原始参数结构通常是固定的 raw_data { hlpretag: span class\s-fc7\, hlposttag: /span, s: keyword, # 搜索关键词 type: 1, # 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户 offset: 0, total: true, limit: 30, csrf_token: # 如果需要可以从Cookie中获取 } # 2. 加密参数 encrypted_data encryptor.encrypt_params(raw_data) # 3. 构造请求头和请求体 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, # Cookie: 你的Cookie如果需要登录态 # 获取某些数据需要Cookie } data { params: encrypted_data[params], encSecKey: encrypted_data[encSecKey] } # 4. 发送POST请求 url https://music.163.com/weapi/cloudsearch/get/web?csrf_token resp requests.post(url, headersheaders, datadata) return resp.json() if __name__ __main__: result search_song(周杰伦) print(json.dumps(result, indent2, ensure_asciiFalse))这段代码定义了一个NeteaseMusicEncryptor类它负责与JS加密代码交互。search_song函数演示了如何加密参数并调用搜索API。注意事项不同的API接口其raw_data的结构可能不同。例如获取歌曲详情、评论、歌单列表的原始参数格式都不一样。你需要通过分析浏览器中对应请求的“Request Payload”来获取正确的结构。使用开发者工具的“网络”选项卡找到weapi请求查看其“载荷”Payload部分通常能看到一个params和encSecKey但旁边会有一个“view source”或“view URL-encoded”点击后可以看到加密前的原始文本格式这就是你需要模仿的raw_data。4. 从ExecJS到纯Python实现的进阶使用PyExecJS是快速验证和上手的绝佳方式但它有性能开销并且依赖外部JS环境。对于追求高性能和部署便捷性的项目我们需要用纯Python重写加密逻辑。4.1 AES加密的Python实现Python的cryptography库提供了强大的加密支持。我们需要实现CBC模式、PKCS7填充的AES加密。from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding from cryptography.hazmat.backends import default_backend import base64 def aes_encrypt(text, key, iv): 使用AES-CBC模式PKCS7填充进行加密。 :param text: 待加密的明文字节串 :param key: 密钥字节串16位 :param iv: 初始向量字节串16位 :return: 加密后的字节串 # 创建PKCS7填充器块大小为128位16字节 padder padding.PKCS7(128).padder() # 对明文进行填充 padded_data padder.update(text) padder.finalize() # 创建AES-CBC加密器 cipher Cipher(algorithms.AES(key), modes.CBC(iv), backenddefault_backend()) encryptor cipher.encryptor() # 执行加密 ciphertext encryptor.update(padded_data) encryptor.finalize() return ciphertext # 网易云固定的AES参数 NETEASE_AES_KEY b0CoJUm6Qyw8W8jud # 注意是字节串 NETEASE_AES_IV b0102030405060708 # 使用示例 raw_json {ids:[123456]}.encode(utf-8) first_encrypt aes_encrypt(raw_json, NETEASE_AES_KEY, NETEASE_AES_IV) # 第一次加密的结果还需要进行第二次AES加密用同一个key和iv second_encrypt aes_encrypt(first_encrypt, NETEASE_AES_KEY, NETEASE_AES_IV) params base64.b64encode(second_encrypt).decode(utf-8) print(f纯Python生成的params: {params})4.2 RSA加密随机填充模式的Python实现这是整个逆向中最棘手的部分因为标准RSA库如rsa、Crypto默认不提供网易云使用的这种“随机填充”模式。我们需要手动实现这个填充过程。网易云的RSA填充逻辑大致如下生成一个长度为127字节的随机字符串random_str。将待加密的文本16字节的AES key反转。构建一个待加密的字节串random_str 反转的key。将这个字节串转换为一个大整数。使用RSA公钥指数e和模数n对这个大整数进行模幂运算cipher_int (plain_int ^ e) % n。将结果整数转换为16进制字符串并填充前导零至256字节512个十六进制字符。import random import binascii from math import gcd def rsa_encrypt_with_random_padding(text, pub_key_n_hex, pub_key_e010001): 模拟网易云音乐的RSA随机填充加密。 :param text: 待加密的文本AES key字符串 :param pub_key_n_hex: RSA公钥模数n十六进制字符串 :param pub_key_e: RSA公钥指数e十六进制字符串默认010001 :return: 加密后的十六进制字符串 # 1. 反转文本 reversed_text text[::-1] # 2. 生成127字节的随机字符串模拟前端Math.random()生成的字符 # 注意前端的随机字符串范围通常是 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 random_str .join(random.choices(abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789, k127)) # 3. 构建明文随机字符串 反转的key plaintext random_str reversed_text plaintext_bytes plaintext.encode(utf-8) # 4. 将明文字节转换为大整数 plain_int int.from_bytes(plaintext_bytes, byteorderbig) # 5. 将十六进制的n和e转换为整数 n int(pub_key_n_hex, 16) e int(pub_key_e, 16) # 6. 模幂运算cipher_int (plain_int ^ e) % n cipher_int pow(plain_int, e, n) # 7. 将结果整数转换为16进制字符串并填充前导零至256字节512个十六进制字符 cipher_hex format(cipher_int, x) # 确保长度为512 cipher_hex cipher_hex.rjust(512, 0) # 如果长度超过512取后512位理论上不会因为模运算后长度不会超过n cipher_hex cipher_hex[-512:] return cipher_hex # 网易云RSA公钥模数示例实际很长 NETEASE_RSA_PUB_KEY_N 00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7 NETEASE_RSA_PUB_KEY_E 010001 # 使用示例 aes_key_str 0CoJUm6Qyw8W8jud # 原始的AES key字符串 enc_seckey_hex rsa_encrypt_with_random_padding(aes_key_str, NETEASE_RSA_PUB_KEY_N, NETEASE_RSA_PUB_KEY_E) encSecKey base64.b64encode(binascii.unhexlify(enc_seckey_hex)).decode(utf-8) print(f纯Python生成的encSecKey: {encSecKey})重要提示上面的RSA公钥N是简短的示例实际网易云使用的公钥模数n是一个长达512位1024字符的16进制数。你必须从最新的JS代码中提取出完整的公钥。此外为了确保与服务器端解密兼容随机字符串的生成规则必须与前端JavaScript完全一致。前端通常使用Math.random().toString(36).slice(2)来生成随机字符然后截取或循环至所需长度。我们的Python实现random.choices可能无法完美复现其随机序列但在一次会话中只要random_str是随机的且长度正确服务器通常能解密。对于要求绝对一致的场景可能需要用PyExecJS执行前端的随机数生成代码。5. 常见问题排查与实战技巧在实际操作中你几乎一定会遇到各种问题。下面是我踩过坑后总结的排查清单和技巧。5.1 问题排查速查表问题现象可能原因排查步骤与解决方案请求返回-460错误码加密参数不正确被服务器识别为非法请求。1.核对AES Key/IV和RSA公钥确认是从当前有效的网页JS中提取的版本可能已更新。2.检查原始参数格式raw_data的JSON结构、字段名、字段类型字符串/数字/布尔必须与浏览器抓包看到的一模一样。特别注意中英文符号和转义。3.验证加密函数用同一份raw_data在浏览器Console里执行加密函数对比生成的params和encSecKey是否与你的Python代码输出完全一致。请求返回-2错误码通常意味着csrf_token校验失败或请求缺少必要的Cookie。1.添加Cookie尝试在请求头中携带从浏览器复制的完整Cookie包含MUSIC_U等字段。2.获取csrf_tokencsrf_token通常可以从Cookie中的__csrf字段获取并需要填入raw_data和URL参数中。PyExecJS报语法错误JS代码包含ES6语法而运行时环境如JScript不支持。1.安装Node.js这是最佳解决方案。2.使用在线转换工具将ES6代码转换为ES5不推荐复杂代码容易出错。3.检查JS代码确保提取的代码是完整的没有遗漏依赖的变量或函数。纯Python实现的RSA加密后服务器无法解密随机填充规则与前端不完全一致。1.精确复现随机字符串生成使用PyExecJS执行前端的Math.random().toString(36).slice(2)来生成随机串。2.核对字节顺序确认明文构建时random_str reversed_key的顺序和编码UTF-8是否正确。3.验证大整数运算用一个小型的、已知的RSA密钥对进行测试确保模幂运算pow(plain_int, e, n)结果正确。能搜索但不能获取评论/详情目标接口需要登录态或不同的参数结构。1.分析目标接口单独对你要爬的接口如/weapi/v1/resource/comments进行抓包分析其特有的raw_data格式。2.携带用户Cookie获取用户登录后的CookieMUSIC_U几乎所有用户相关操作都需要它。5.2 实战技巧与心得“锁定”JS版本网易云的JS文件可能会更新。一旦你成功逆向了一套加密逻辑最好将当时使用的核心JS文件如core.js保存下来。这样即使网站更新你依然可以用旧版本的逻辑进行测试和对比快速定位变化点。使用“本地替换”进行调试在开发者工具中你可以将网络请求的JS文件映射到本地修改过的文件。这样你可以在本地JS中插入console.log或debugger语句详细跟踪加密每一步的中间结果极大提升调试效率。关注csrf_token这个令牌有时可以为空但对于涉及用户状态的操作点赞、收藏、发评论是必须的。它通常存在于Cookie中也可能在页面HTML的某个meta标签里。你的爬虫需要维护会话使用requests.Session来自动管理Cookie并在需要时提取csrf_token。参数化你的加密器不要将AES Key、IV、RSA公钥等硬编码在主要逻辑里。将它们作为配置项或类属性方便未来替换。因为一旦网易云更换这些密钥你只需要更新配置而不用修改核心代码。处理速率限制网易云对频繁请求有风控。在你的爬虫中加入合理的延时如time.sleep(random.uniform(1, 3))并使用代理IP池避免IP被封锁。从ExecJS过渡到纯Python建议分两步走。第一步用PyExecJS实现功能确保整个数据流能跑通。第二步在PyExecJS版本的基础上添加详细的日志打印出加密过程中每一步的中间结果如第一次AES加密后的字节、反转前的key、随机字符串、RSA加密前的整数等。然后用这些中间结果作为“测试用例”来调试和验证你的纯Python实现确保两者输出在每个环节都完全一致。逆向工程没有一成不变的解决方案核心在于理解和分析。网易云音乐的加密方案是一个经典的案例掌握了它你就拿到了进入现代Web逆向世界的一把重要钥匙。剩下的就是不断练习培养那种在混乱的混淆代码中寻找关键线索的“嗅觉”。