Python逆向网易云音乐评论加密:AES+RSA混合加密实战解析
1. 项目概述与核心目标
最近在分析一些音乐社区的数据时,不可避免地遇到了网易云音乐这个“老朋友”。它的评论区藏着大量有价值的用户情绪和反馈,对于做舆情分析、内容推荐或者单纯想研究用户行为的人来说,是个宝库。但稍微尝试一下就知道,直接请求接口拿到的评论数据,是一堆看不懂的加密字符串。这层加密机制,就像一扇紧锁的门,把原始数据保护了起来。今天要聊的,就是如何用Python这把“钥匙”,结合2024年最新的网页端技术动态,把这扇门打开,还原出明文的评论内容。这不是一个简单的爬虫教程,而是一次完整的逆向工程实战,我们会深入到JavaScript混淆、加密算法识别和Python模拟执行的细节中。无论你是对数据抓取感兴趣,还是想学习现代Web应用的反爬虫策略如何破解,这篇文章都能给你提供一条清晰的路径。我会假设你已经有基础的Python和HTTP知识,但对逆向不熟悉也没关系,我会把关键步骤拆解得足够细。
2. 逆向工程的核心思路与准备工作
逆向网络请求,核心目标就是找到客户端(浏览器)在发送请求前,对原始数据做了哪些处理,并在我们的Python脚本里完美复现这个过程。对于网易云音乐评论接口,这个处理通常发生在JavaScript代码中,并且代码会被高度混淆,增加阅读和理解的难度。
2.1 工具链准备:你的数字手术刀
工欲善其事,必先利其器。逆向工作流依赖几个关键工具,它们各有专长。
浏览器开发者工具(Chrome DevTools):这是我们的主战场。核心功能包括:
- Network(网络)面板:记录所有HTTP/HTTPS请求。重点关注
XHR和Fetch类型的请求,评论数据通常通过它们加载。你需要在这里找到获取评论的那个关键请求。 - Sources(源代码)面板/调试器:用于查看和调试页面加载的JavaScript文件。当我们在Network面板找到目标请求后,可以点击其
Initiator(发起者)标签,直接跳转到发起这个请求的JS代码行。 - Console(控制台):一个强大的JavaScript执行环境。我们可以在这里直接调用或测试我们发现的加密函数,验证其功能。
Node.js 与 PyExecJS / js2py:网易云音乐的加密逻辑是用JS写的,我们要在Python环境中复现它。有两种主流思路:
- 使用PyExecJS:这个库相当于一个桥梁,允许Python调用本机安装的JavaScript运行时(如Node.js)来执行JS代码。它的优点是能执行复杂的、包含浏览器环境特定对象的JS代码(前提是你能模拟出这些对象),兼容性好。
- 使用js2py:这是一个纯Python实现的JavaScript解释器。它的优点是不依赖外部环境,部署简单。但对于非常复杂或使用了最新ES特性的混淆代码,可能会遇到兼容性问题或性能瓶颈。 我的建议是,在开发调试阶段使用
PyExecJS配合Node.js,因为Node.js环境更标准,调试信息更清晰。如果最终部署的环境不方便安装Node.js,再考虑将关键JS函数抽离、简化后,用js2py来执行。
代码格式化与搜索工具:线上JS代码通常被压缩成一行,无法阅读。DevTools的Sources面板自带格式化按钮({})。格式化后,你需要使用强大的搜索功能(Ctrl+Shift+F)在整个项目代码中搜索关键参数名,如加密请求体中的特定字段名(例如params,encSecKey等),这是定位加密函数的起点。
2.2 关键请求定位与参数分析
首先,我们得找到“敌人在哪里”。打开网易云音乐网页版,进入任意一首歌曲的页面,打开开发者工具的Network面板,然后触发评论加载(比如翻到评论底部触发加载更多)。
- 筛选请求:在Network面板中,筛选
XHR或Fetch请求。你会看到一系列请求,其中很可能包含类似weapi/v1/resource/comments/R_SO_4_{歌曲ID}?csrf_token=这样的请求。这就是获取评论列表的接口。 - 分析请求载荷:点击这个请求,查看
Headers和Payload(在Fetch/XHR请求下可能是Request Payload或Form Data)。你会发现,它的请求方式很可能是POST,并且携带了两个非常关键的、看起来是加密过的参数:params和encSecKey。这两个参数就是一长串无规律的十六进制字符串,这就是我们需要攻破的加密结果。 - 追踪发起者:在该请求的详情中,找到
Initiator列,点击旁边的小链接,它会把我们直接带到发起这个网络请求的JavaScript代码处。这里就是加密发生的源头。
注意:网易云音乐的接口和参数名可能会随时间更新。
params和encSecKey是历史上长期使用的命名,但务必以你实际抓包看到的名称为准。如果不同,就用你看到的名称作为搜索关键词。
3. 加密机制深度解析与JS代码定位
找到发起请求的代码行只是第一步,通常那只是一个调用点,类似ajax.post(url, {params: encryptedParams, encSecKey: encSecKey})。真正的加密函数在别处。我们需要逆向推导出生成params和encSecKey的完整逻辑。
3.1 基于搜索的代码定位法
在Sources面板中,对格式化后的所有JS代码进行全局搜索(Ctrl+Shift+F)。
- 第一轮搜索:搜索
params:或encSecKey:。这能帮你找到构建请求数据的代码块。 - 第二轮搜索:在上一步找到的代码块附近,查找这些参数的赋值语句。它们很可能来自于某个函数的返回值,比如
params = window.asrsea(JSON.stringify(data), key1, key2, key3)。这个window.asrsea或者类似的函数名(可能是极度混淆后的名字,如window.abcdefg)就是核心加密函数。 - 第三轮搜索:搜索这个函数名(如
asrsea),找到它的函数定义。这里就是所有加密魔法发生的地方。
3.2. 核心加密函数逆向(以常见模式为例)
经过多次逆向分析,网易云音乐网页版的加密曾采用一种相对固定的模式,其核心是一个自定义函数(常被命名为window.asrsea),内部组合使用了AES加密和RSA加密。理解这个模式对破解至关重要。
典型的加密流程如下:
- 构建明文数据:首先,客户端会将需要发送的数据(例如
{rid: “R_SO_4_歌曲ID”, offset: 0, limit: 20})转换为JSON字符串。我们称这个字符串为text。 - 生成随机密钥:在客户端,会生成一个16字节的随机字符串,作为AES加密的密钥,我们称之为
secKey。这个secKey是每次请求都可能变化的。 - AES加密数据:使用上一步生成的
secKey作为密钥,对text进行AES-128-CBC模式加密。加密后得到密文数据。这个密文数据经过Base64编码后,就生成了我们看到的params参数。params是数据的密文。 - RSA加密密钥:客户端有一个预设的、固定的RSA公钥。使用这个公钥,对刚才随机生成的
secKey进行RSA加密。加密后的结果,就生成了encSecKey参数。encSecKey是密钥(secKey)的密文。
为什么这样设计?这是一种典型的“混合加密”机制,兼具了对称加密和非对称加密的优点:
- AES(对称加密):加密解密速度快,适合加密大量数据(我们的评论请求体)。
- RSA(非对称加密):加密速度慢,但用公钥加密后,只有持有对应私钥的服务器才能解密。这里用它来加密随机的AES密钥(
secKey)。 - 流程:服务器收到
params和encSecKey后,先用其私钥解密encSecKey,得到本次请求的secKey,再用这个secKey去解密params,得到原始的请求数据。这样既保证了数据传输的效率,又保证了密钥交换的安全。
实操心得:在2024年的最新版本中,核心的AES+RSA混合加密模式很可能依然保持不变,这是兼顾安全与性能的经典设计。但为了对抗自动化分析,网易云音乐极有可能在以下方面升级:1)混淆强度加大:函数名、变量名可能变成无意义的乱码,流程控制更复杂。2)密钥生成逻辑微调:AES密钥(
secKey)的生成可能引入了更多与客户端环境相关的因子(如时间戳哈希、某个DOM元素的属性等)。3)加入反调试:JS代码中可能包含检测开发者工具是否打开的代码,导致调试时逻辑分支不同。遇到这些情况,需要更多的耐心和动态调试技巧。
3.3 提取并简化加密函数
找到window.asrsea(或类似函数)的定义后,你需要将其完整的JS代码复制出来。这个函数往往依赖其他一些辅助函数或全局变量(比如一个巨大的、包含RSA公钥的常量对象)。你必须将这些依赖一并找到并复制。
关键步骤:
- 在Console中,尝试直接调用
window.asrsea(JSON.stringify({“rid”: “R_SO_4_xxxx”}), “xxx”, “xxx”, “xxx”),看看能否复现出和网络请求中一样的params和encSecKey。如果能,说明你提取的代码是完整的。 - 将完整的、可运行的加密JS代码保存到一个文件中,例如
encrypt.js。 - 简化:为了在Python中高效调用,我们可以对这个JS文件进行适当简化。移除所有与加密核心逻辑无关的代码(比如UI操作、事件绑定等)。确保剩下的代码是一个“纯函数”,它接收明文数据,输出
params和encSecKey。
4. Python模拟加密的完整实现
现在,我们有了核心的JS加密代码,目标是在Python中模拟浏览器行为,对任意给定的请求数据生成正确的加密参数。
4.1 方案选择:PyExecJS动态执行
这里我们采用PyExecjs方案,因为它能更好地处理复杂的JS环境。
首先安装必要的库:
pip install pyexecjs requests确保你的系统已经安装了Node.js,因为PyExecJS默认会使用它作为运行时。
4.2 构建Python加密模块
我们创建一个Python类来封装加密逻辑。
import json import execjs import requests import time import random from typing import Dict, Any class NeteaseMusicEncryptor: def __init__(self, js_file_path='encrypt.js'): """ 初始化加密器,加载JS代码。 :param js_file_path: 包含核心加密函数的JS文件路径 """ with open(js_file_path, 'r', encoding='utf-8') as f: js_code = f.read() # 创建JS执行上下文 self.ctx = execjs.compile(js_code) # 假设我们提取的JS函数名为 `window.asrsea`,在ExecJS中调用时需注意 # 如果JS代码最后将函数导出为模块,可能需要调整调用方式。 # 这里假设加密函数在JS中定义为 `function encrypt(data) {...}` 并暴露给了全局。 # 实际情况需要根据你的 `encrypt.js` 文件调整。 # 例如,如果你的JS里是 `window.encrypt = function(text, key1, key2, key3){...}` # 那么调用就是 `self.ctx.call('window.encrypt', ...)` def _call_js_encrypt(self, text: str) -> Dict[str, str]: """ 调用JS加密函数。 这是最需要根据实际JS代码调整的部分。 """ # 情况一:JS函数接收明文文本,返回一个包含params和encSecKey的对象 # result = self.ctx.call('encrypt', text) # return {'params': result['params'], 'encSecKey': result['encSecKey']} # 情况二:JS函数接收多个参数(如固定字符串key),模仿原始调用 # 你需要从原始JS调用处确定这些参数是什么。历史上网易云用过 `"010001"`, `"00e0b509f...很长的一个字符串"`, `"0CoJUm6Qyw8W8jud"` 等。 # 例如: # encText, encSecKey = self.ctx.call('window.asrsea', text, “010001”, “00e0b509f...”, “0CoJUm6Qyw8W8jud”) # return {'params': encText, 'encSecKey': encSecKey} # 这里是一个通用示例,你需要替换函数名和参数 try: # 假设你的JS代码暴露了一个叫 `encrypt` 的函数 result = self.ctx.call('encrypt', text) return result except Exception as e: print(f“调用JS加密函数失败: {e}”) # 尝试打印JS环境中的可用函数,辅助调试 # print(self.ctx.eval('Object.keys(globalThis).filter(k => typeof globalThis[k] === “function”)')) raise def generate_encrypted_params(self, request_data: Dict[str, Any]) -> Dict[str, str]: """ 主函数:接收一个字典形式的请求数据,返回加密后的参数字典。 :param request_data: 例如 {‘rid’: ‘R_SO_4_186016’, ‘offset’: 0, ‘limit’: 20, ‘csrf_token’: ‘’} :return: 包含加密后 params 和 encSecKey 的字典 """ # 1. 将请求数据转换为JSON字符串 text = json.dumps(request_data, separators=(‘,’, ‘:’)) # 去除空格,与浏览器行为一致 # 2. 调用JS加密函数 encrypted = self._call_js_encrypt(text) # 3. 返回加密参数 return {‘params’: encrypted[‘params’], ‘encSecKey’: encrypted[‘encSecKey’]} # 示例:获取加密参数的辅助函数 def get_comment_encrypted_params(song_id: str, offset: int = 0, limit: int = 20): encryptor = NeteaseMusicEncryptor(‘encrypt.js’) request_data = { ‘rid’: f‘R_SO_4_{song_id}’, # 评论资源ID,歌曲评论固定格式 ‘offset’: offset, ‘limit’: limit, ‘total’: ‘true’, # 可能需要的字段,以实际抓包为准 ‘csrf_token’: ‘’, # 通常为空,但需要包含此字段 } return encryptor.generate_encrypted_params(request_data)4.3 发起请求与解密响应
加密参数准备好后,发起POST请求就很简单了。响应的数据通常是明文的JSON,但有时也可能被加密。对于评论接口,目前测试返回的是明文。
def fetch_comments(song_id: str, offset: int = 0, limit: int = 20): """ 获取网易云音乐歌曲评论 """ url = “https://music.163.com/weapi/v1/resource/comments/R_SO_4_{}?csrf_token=”.format(song_id) headers = { ‘User-Agent’: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36’, ‘Referer’: ‘https://music.163.com/’, ‘Content-Type’: ‘application/x-www-form-urlencoded’, # 注意:虽然是JSON数据,但表单方式提交 ‘Origin’: ‘https://music.163.com’, } # 获取加密后的参数 encrypted_params = get_comment_encrypted_params(song_id, offset, limit) # 发起请求 resp = requests.post(url, headers=headers, data=encrypted_params) resp.raise_for_status() # 解析JSON响应 comment_data = resp.json() return comment_data # 使用示例 if __name__ == ‘__main__’: # 示例歌曲ID:光辉岁月 song_id = ‘186016’ try: data = fetch_comments(song_id) comments = data.get(‘comments’, []) for comment in comments[:5]: # 打印前5条评论 user = comment[‘user’][‘nickname’] content = comment[‘content’] print(f‘{user}: {content}’) print(f‘总共获取到 {len(comments)} 条评论’) except Exception as e: print(f‘请求失败: {e}’)5. 动态对抗与常见问题排查
逆向工程不是一劳永逸的,网站会更新,防御会升级。以下是你在实战中几乎一定会遇到的问题和解决思路。
5.1 常见问题速查表
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
params/encSecKey生成错误 | 1. JS代码提取不完整,缺少依赖。 2. 加密函数调用方式或参数错误。 3. 随机因子(如 secKey)生成逻辑未完全模拟。 | 1.完整性检查:在浏览器Console中,用相同的输入调用你提取的JS函数,对比输出是否与网络请求完全一致。 2.参数比对:确保传递给JS函数的每一个参数(包括那些看似固定的字符串)都与浏览器端调用时完全一致。一个字符都不能差。 3.动态调试:在浏览器Sources面板中,在加密函数入口打上断点,单步执行,记录下每一步的关键变量值,与你的Python脚本中的逻辑进行比对。 |
请求返回-460或-462等错误码 | 1. 请求头不完整或错误,特别是Cookie和User-Agent。2. 缺少必要的 csrf_token。3. 请求频率过高,触发风控。 | 1.补全请求头:使用浏览器中捕获的完整请求头,尤其是Cookie。可以先用requests.Session()对象保持会话。2.获取csrf_token: csrf_token通常藏在页面HTML或某个初始化接口的响应里。你需要先访问一次主页面,用正则或解析库提取出来。3.降低频率,添加延迟:在请求间加入随机延时(如 time.sleep(random.uniform(1, 3))),模拟真人操作。 |
| JS代码包含反调试 | 代码中可能有debugger语句或检测控制台打开的代码,导致在调试模式下无法正常执行或进入死循环。 | 1.禁用断点:在DevTools设置中暂时禁用所有断点。 2.重写函数:在JS代码开头,重写 console.log或debugger关键字,使其失效。例如:var debugger = function(){};或console.log = function(){};(注意,这可能会影响代码逻辑,需谨慎)。3.补环境:如果反调试是检测 window.outerHeight等属性,需要在Node.js环境中模拟这些属性。 |
PyExecJS执行报错 | 1. Node.js环境问题。 2. JS代码中存在浏览器特有的全局对象(如 window,document,navigator)。 | 1.检查Node:在命令行输入node -v和execjs.get().name确认环境。2.补环境:这是逆向中的高级技巧。在你的 encrypt.js文件开头,手动定义这些浏览器对象。例如:var window = this; var document = {}; var navigator = {userAgent: ‘…’};。需要的对象可以通过在浏览器Console中打印Object.keys(window)来查找。 |
| 加密算法或密钥已更新 | 服务器端更新了RSA公钥或AES的模式、填充方式。 | 1.重新抓包分析:这是根本解决方法。重复第2、3步,定位新的加密函数和密钥。 2.关注固定参数:对比新旧JS代码中那些看似固定的字符串(如RSA公钥),看是否发生变化。 |
5.2 高级技巧:补环境(Anti-Anti-Sandbox)
当JS代码严重依赖浏览器环境时,简单的execjs执行会报错ReferenceError: window is not defined。你需要“补全”这个环境。
创建一个env.js文件,在加载核心加密代码前执行它:
// env.js - 模拟浏览器全局对象 var window = this; var document = { createElement: function() { return {}; }, // 根据需要添加其他属性 }; var navigator = { userAgent: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 …’ }; var location = {}; var screen = {}; // 如果有特定的函数或对象被检测,也需要在这里定义 // 例如,如果代码用了 `window.btoa`,你需要 polyfill if (!window.btoa) { window.btoa = function(str) { return Buffer.from(str).toString(‘base64’); }; } if (!window.atob) { window.atob = function(b64Encoded) { return Buffer.from(b64Encoded, ‘base64’).toString(); }; }然后,在你的Python中,先加载env.js,再加载你的encrypt.js:
with open(‘env.js’, ‘r’, encoding=‘utf-8’) as f: env_code = f.read() with open(‘encrypt.js’, ‘r’, encoding=‘utf-8’) as f: encrypt_code = f.read() full_js_code = env_code + ‘\n’ + encrypt_code ctx = execjs.compile(full_js_code)5.3 保持代码的健壮性
- 异常处理:对所有网络请求和JS调用进行
try…except包装,记录日志,便于排查。 - 参数化配置:将歌曲ID、请求头、接口URL等写成配置文件或函数参数,提高代码复用性。
- Session管理:使用
requests.Session()来管理Cookie,模拟一个持续的会话状态。 - 更新机制:意识到加密逻辑可能会变,设计一个简单的版本检查或失败重试机制,一旦发现旧的加密方式失效,能触发告警。
逆向工程是一场与防御策略的持续博弈。2024年网易云音乐的加密机制,其核心思想可能依然稳固,但实现细节的“外壳”一定会更加坚硬和复杂。成功的关键在于耐心、细致的观察力和对HTTP/HTTPS、JavaScript以及加密学基础的理解。通过这次实战,你掌握的不仅仅是如何获取网易云音乐的评论,更是一套应对类似前端加密场景的通用方法论。记住,尊重数据版权和网站的服务条款,将技术用于学习和有益的数据分析。
