1. 项目概述与核心价值
最近在技术社区里,看到不少朋友对音乐平台的接口逆向感兴趣,尤其是网易云音乐,它的加密机制一直是个“小挑战”。今天,我就以一个实际项目为例,手把手带你走一遍用Python逆向网易云音乐加密接口,实现歌曲搜索和下载的全过程。这不仅仅是一个爬虫脚本,更是一次对Web前端加密、网络协议分析和Python实战的深度探索。无论你是想批量下载自己喜欢的歌单,还是单纯对爬虫和逆向技术感到好奇,这篇文章都能给你提供一条清晰的路径和可直接运行的完整代码。
简单来说,我们要做的,就是模拟一个真实的网易云音乐客户端,向它的服务器发送请求,拿到经过加密处理的歌曲数据,然后解密、解析,最终把音乐文件保存到本地。整个过程涉及对网页端JavaScript加密逻辑的分析、Python网络请求库的灵活运用,以及对特定数据格式的处理。我会把每个步骤拆开揉碎了讲,包括我踩过的坑和总结的技巧,确保你跟着做就能出结果。当然,所有操作都应在法律允许和个人授权的范围内进行,仅用于学习交流目的。
2. 逆向目标分析与思路拆解
2.1 网易云音乐接口加密机制初探
要逆向,首先得知道对方是怎么“锁门”的。通过浏览器开发者工具(F12)分析网易云音乐网页版的网络请求,你会发现几个关键特征。搜索歌曲、获取歌曲详情、获取播放链接等核心接口,其请求参数和响应数据都不是明文的。一个典型的特征是,请求体(Request Payload)通常是一个名为“params”和“encSecKey”的加密字符串对,这正是网易云音乐采用的自定义加密方案。
这种方案的核心在前端。网页加载时,会执行一段复杂的JavaScript代码,这段代码负责将用户输入的查询关键词(比如“周杰伦”)以及一些固定参数,通过特定的加密算法,生成最终的“params”和“encSecKey”。服务器收到这对加密字符串后,用对应的私钥解密,验证请求合法性,再返回加密或经过处理的响应数据。所以,我们的逆向主战场就在这段前端JS代码里。目标不是破解算法(那是密码学家的事),而是理解其加密流程,并用Python完整地复现这个过程,让我们的Python脚本能生成服务器认可的“钥匙”。
2.2 核心逆向思路与工具选型
我们的核心思路是“模拟”,而非“破解”。具体分为三步走:
- 定位加密函数:使用浏览器开发者工具的“搜索”功能,在加载的JS文件中搜索关键参数名,如“params”、“encSecKey”、“加密”等中文或拼音,快速定位到执行加密操作的函数所在位置。
- 分析加密流程:仔细阅读相关函数,理清其输入(原始参数)、输出(加密后的params和encSecKey)以及中间调用的其他函数(如AES、RSA等)。通常,网易云音乐会采用AES加密内容,再用RSA加密AES的密钥,这是一种混合加密模式。
- Python复现:将JS的加密逻辑,用Python的加密库(如
Crypto或cryptography)重新实现。关键在于确保每一步使用的算法模式(如AES-CBC)、填充方式(如PKCS7)、密钥、初始向量(IV)等参数与JS端完全一致。
工具选型:
- 浏览器:Chrome或Edge,其开发者工具功能强大。
- Python库:
requests:用于发送HTTP请求。json:用于处理请求和响应的数据。cryptography或pycryptodome:用于实现AES、RSA加密。我个人更推荐pycryptodome,它对各种加密模式的支持更友好,API也更接近我们常见的认知。re/execjs:有时JS代码过于复杂或混淆严重,可以考虑用execjs库直接执行关键的JS函数片段。但优先推荐纯Python复现,性能更好,依赖更清晰。
注意:网易云音乐的加密逻辑可能会随版本更新而变化。本文基于当前(撰写时)可用的分析,核心思路不变,但具体函数名、密钥等可能需要你根据实际情况稍作调整。逆向本身就是一个“斗智斗勇”的动态过程。
3. 关键加密逻辑分析与Python复现
3.1 定位并解析前端加密代码
打开网易云音乐网页版,进入搜索页面。打开开发者工具(F12),切换到“网络”(Network)选项卡,勾选“保留日志”(Preserve log)。在搜索框输入一个关键词(例如“测试”),点击搜索,观察网络请求。
你会发现一个名为cloudsearch?csrf_token=的请求(或类似search的接口)。点击该请求,查看其“负载”(Payload)或“请求体”(Request Body),通常是以application/x-www-form-urlencoded格式发送的,里面包含了params和encSecKey这两个长长的字符串。
接下来是关键:在“源代码”(Sources)或“网络”(Network)中查看返回这个页面的主JS文件,或者使用“搜索”(Search)面板,在所有加载的资源中搜索“encSecKey”。很快,你会找到一个包含大量函数定义的JS文件,其中有一个函数(可能叫window.asrsea或d等)负责生成这两个参数。
通过断点调试或仔细阅读,我们可以梳理出典型的加密流程:
- 构造一个原始数据对象,包含搜索词、分页参数等。
- 将这个对象转换成JSON字符串。
- 对JSON字符串进行AES加密,生成密文。这里会用到:
- 一个固定的AES密钥(16位字符串)。
- 一个固定的初始向量IV(16位字符串)。
- 模式通常是CBC。
- 填充是PKCS7。
- 对第2步使用的AES密钥,进行RSA加密。这里会用到:
- 一个固定的RSA公钥(通常是一个很长的16进制字符串或Base64编码)。
- 指数(exponent)通常是
010001(65537的16进制)。
- 最终,AES加密后的密文经过Base64编码,成为
params;RSA加密后的AES密钥经过16进制编码,成为encSecKey。
3.2 使用Python实现加密函数
理解了流程,我们就可以用Python来复现了。首先安装必要的库:pip install requests pycryptodome。
下面是一个核心加密函数的示例实现。请注意,其中的AES_KEY,AES_IV,RSA_PUBLIC_KEY需要你从分析出的JS代码中提取,它们通常是固定的常量。
import json import base64 from Crypto.Cipher import AES, PKCS1_v1_5 from Crypto.PublicKey import RSA from Crypto.Util.Padding import pad, unpad import binascii # !!!重要:以下密钥需从目标网站JS代码中提取,此处为示例格式!!! # AES加密所需的密钥和初始向量 (16字节,即128位) AES_KEY = '0CoJUm6Qyw8W8jud' # 示例,需替换 AES_IV = '0102030405060708' # 示例,需替换 # RSA公钥 (去头尾和换行,纯内容) RSA_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB -----END PUBLIC KEY-----""" def encrypt_params(data): """ 模拟网易云音乐前端加密,生成 params 和 encSecKey :param data: 原始的请求参数字典,例如 {'s': '周杰伦', 'type': 1, 'offset': 0} :return: (params, encSecKey) """ # 1. 将原始数据转为JSON字符串 text = json.dumps(data) # 2. 第一次AES加密 (使用固定密钥) params = aes_encrypt(text, AES_KEY, AES_IV) # 3. 第二次AES加密 (使用随机密钥,但网易云前端实际使用了一个固定密钥,这里我们按固定密钥处理) # 注意:实际JS中可能进行了两次AES加密,第二次的密钥是固定的另一个值或随机生成。 # 需要根据具体JS逻辑调整。这里假设第二次加密的密钥是固定的。 # 假设第二次加密的密钥是 `AES_KEY`,实际请根据JS代码确认。 params = aes_encrypt(params, AES_KEY, AES_IV) # 对最终密文进行Base64编码 params = base64.b64encode(params.encode('utf-8')).decode('utf-8') # 4. 对AES_KEY进行RSA加密生成encSecKey # 注意:RSA加密的原文是AES_KEY,还是经过处理的?需要看JS。 # 常见情况是:对一个16字节的随机字符串(或固定字符串)进行RSA加密。 # 这里我们假设就是对 `AES_KEY` 这个字符串进行RSA加密。 encSecKey = rsa_encrypt(AES_KEY, RSA_PUBLIC_KEY) return params, encSecKey def aes_encrypt(plain_text, key, iv): """AES-CBC加密,PKCS7填充""" cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8')) padded_data = pad(plain_text.encode('utf-8'), AES.block_size) encrypted_data = cipher.encrypt(padded_data) # 返回Base64编码的字符串,但外层函数可能还需要处理,这里先返回字节 return base64.b64encode(encrypted_data).decode('utf-8') def rsa_encrypt(plain_text, public_key): """RSA加密""" key = RSA.import_key(public_key) cipher = PKCS1_v1_5.new(key) # RSA加密有长度限制,AES_KEY长度通常符合要求 encrypted_data = cipher.encrypt(plain_text.encode('utf-8')) # 网易云前端返回的是16进制大写字符串 return encrypted_data.hex().upper() # 测试加密函数 if __name__ == '__main__': test_data = {'s': '测试歌曲', 'type': 1, 'offset': 0, 'limit': 20} params, encSecKey = encrypt_params(test_data) print(f"生成的 params: {params[:50]}...") print(f"生成的 encSecKey: {encSecKey[:50]}...")关键点解析:
- 两次AES加密:你可能在JS中看到两次加密。第一次的明文是JSON文本,第二次的明文是第一次加密后的密文。两次的密钥和IV可能相同,也可能不同(第二次有时用一个随机字符串作为密钥,但这个“随机”字符串在单次请求中是固定的)。你必须根据你抓取到的具体JS代码逻辑来确定。上述代码注释部分已强调。
- RSA加密对象:RSA加密的并不一定是原始的
AES_KEY。有时前端会生成一个16位的随机字符串作为“真实”的AES密钥(称为secretKey),用这个secretKey进行AES加密,然后再用RSA公钥加密这个secretKey。encSecKey就是加密后的secretKey。而params是用secretKey进行AES加密的结果。这是更常见的混合加密模式。你需要确认JS中encSecKey到底是由哪个字符串加密得来。 - 逆向的精髓:没有一成不变的代码。上面的函数是一个框架,
AES_KEY,AES_IV,RSA_PUBLIC_KEY以及加密的具体步骤(如AES加密次数、RSA加密的原文)都需要你通过分析当前的网易云音乐网页JS代码来确认。你可以通过将你的Python加密结果与浏览器实际发送的请求参数对比,来验证你的复现是否正确。
4. 构建完整的搜索与下载功能
4.1 模拟搜索请求与解析结果
有了加密函数,我们就可以模拟搜索请求了。搜索接口通常是https://music.163.com/weapi/cloudsearch/get/web?csrf_token=。我们需要用POST方法,提交表单数据,其中包含我们加密生成的params和encSecKey。
import requests def search_song(keyword, limit=30, offset=0, search_type=1): """ 搜索歌曲 :param keyword: 搜索关键词 :param limit: 返回数量 :param offset: 偏移量,用于分页 :param search_type: 搜索类型,1为单曲,10为专辑,100为歌手... :return: 返回结果列表 """ url = 'https://music.163.com/weapi/cloudsearch/get/web?csrf_token=' # 构造原始请求数据 raw_data = { 's': keyword, 'type': search_type, 'limit': limit, 'offset': offset, # 可能还有其他固定参数,如 'total': True, 'csrf_token': '' 等,需从JS或请求中复制 } # 加密数据 params, encSecKey = encrypt_params(raw_data) # 构造表单数据 form_data = { 'params': params, 'encSecKey': encSecKey } # 设置请求头,模拟浏览器 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', 'Origin': 'https://music.163.com' } try: response = requests.post(url, data=form_data, headers=headers, timeout=10) response.raise_for_status() # 检查请求是否成功 result_json = response.json() if result_json.get('code') == 200: songs = result_json.get('result', {}).get('songs', []) return songs else: print(f"搜索失败,代码: {result_json.get('code')}, 消息: {result_json.get('msg')}") return [] except requests.exceptions.RequestException as e: print(f"网络请求异常: {e}") return [] except json.JSONDecodeError as e: print(f"响应解析异常: {e}") return [] # 测试搜索 if __name__ == '__main__': songs = search_song('周杰伦', limit=5) for idx, song in enumerate(songs): song_id = song.get('id') song_name = song.get('name') artists = '、'.join([ar.get('name') for ar in song.get('ar', [])]) album_name = song.get('al', {}).get('name') print(f"{idx+1}. ID:{song_id} 歌曲:{song_name} 艺术家:{artists} 专辑:{album_name}")这段代码会返回一个歌曲列表,里面包含了歌曲ID、名称、艺术家、专辑等信息。歌曲ID是后续获取下载链接的关键。
4.2 获取歌曲播放链接与解密
搜索到歌曲ID后,我们需要获取其可播放或可下载的音频文件链接。网易云音乐歌曲的播放信息通常通过另一个接口获取,例如https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=。这个接口同样需要加密参数,并且它返回的播放链接(url字段)有时是临时的、有有效期的,甚至可能是经过进一步编码或需要额外解密的。
def get_song_url(song_id, bitrate=320000): """ 获取歌曲播放链接 :param song_id: 歌曲ID :param bitrate: 音质,默认320000 (320kbps),可选 999000 (无损) :return: 歌曲播放链接,若无则返回None """ url = 'https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=' raw_data = { 'ids': f'[{song_id}]', # 注意这里是数组格式的字符串 'level': 'standard', # 或 'exhigh', 'lossless', 'hires' 对应不同音质 'encodeType': 'aac', # 或 'mp3' # 注意:`level`和`encodeType`的映射关系,以及`bitrate`参数可能已变化。 # 更常见的参数是 `br`: bitrate, 例如 `br: 320000` 'br': bitrate, } # 重要:这个接口的原始数据格式和加密所需的固定参数可能与搜索接口不同! # 你需要像分析搜索接口一样,去分析播放链接接口的JS加密逻辑。 # 可能需要修改 encrypt_params 函数,或为这个接口单独写一个加密函数。 params, encSecKey = encrypt_params(raw_data) # 这里假设加密函数通用,但很可能需要调整 form_data = {'params': params, 'encSecKey': encSecKey} headers = { /* 同上,略 */ } try: response = requests.post(url, data=form_data, headers=headers) result = response.json() if result.get('code') == 200: data = result.get('data', []) if data: song_info = data[0] # 检查url是否存在且有效 song_url = song_info.get('url') if song_url and song_info.get('code') == 200: return song_url else: print(f"歌曲 {song_id} 无法获取链接,原因: {song_info.get('code')}") return None else: print(f"获取播放链接失败: {result}") return None except Exception as e: print(f"获取播放链接异常: {e}") return None重要提醒:获取播放链接的接口,其请求参数结构、加密所需的固定字段(可能除了params和encSecKey还有别的)很可能与搜索接口不同。你必须独立分析这个接口的JS加密逻辑。一个常见的不同点是,它可能需要一个叫csrf_token的字段,这个token可以从页面Cookie或之前的响应中获取。不要直接套用搜索接口的加密函数。
4.3 实现歌曲下载功能
拿到真正的音频文件链接后,下载就很简单了,就是一个普通的GET请求,将响应内容以二进制形式写入文件。
def download_song(song_url, song_name, artist, save_path='./downloads'): """ 下载歌曲 :param song_url: 音频文件直链 :param song_name: 歌曲名(用于命名文件) :param artist: 艺术家名(用于命名文件) :param save_path: 保存目录 """ import os if not os.path.exists(save_path): os.makedirs(save_path) # 清理文件名中的非法字符 def sanitize_filename(filename): return re.sub(r'[<>:\"/\\|?*]', '_', filename) safe_song_name = sanitize_filename(song_name) safe_artist = sanitize_filename(artist) filename = f"{safe_artist} - {safe_song_name}.mp3" filepath = os.path.join(save_path, filename) 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/', } try: print(f"正在下载: {filename}") response = requests.get(song_url, headers=headers, stream=True, timeout=30) response.raise_for_status() with open(filepath, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) print(f"下载完成: {filepath}") return True except requests.exceptions.RequestException as e: print(f"下载失败 {filename}: {e}") return False except IOError as e: print(f"文件写入失败 {filename}: {e}") return False # 整合流程示例 def main_download_workflow(keyword): """整合搜索、获取链接、下载的完整流程示例""" songs = search_song(keyword, limit=1) if not songs: print("未搜索到歌曲。") return song = songs[0] song_id = song.get('id') song_name = song.get('name') artist = '、'.join([ar.get('name') for ar in song.get('ar', [])]) print(f"找到歌曲: {song_name} - {artist}") song_url = get_song_url(song_id) if song_url: download_song(song_url, song_name, artist) else: print("无法获取歌曲下载链接。")5. 常见问题、调试技巧与注意事项
5.1 加密结果比对与调试
逆向中最关键的一步是验证你的Python加密结果是否和浏览器一致。
- 抓取真实请求:在浏览器中完成一次搜索,在开发者工具的“网络”选项卡中,找到搜索请求,查看其“负载”(Payload),复制下
params和encSecKey的值。 - Python脚本输出:在你的Python脚本中,打印出针对完全相同的搜索关键词和参数生成的
params和encSecKey。 - 逐层比对:
- 如果完全一致,恭喜你,加密复现成功。
- 如果不一致,问题通常出在:
- 原始数据构造:检查你的
raw_data字典是否和浏览器发送的一模一样(包括所有键值对,甚至顺序?有时顺序有影响)。仔细查看浏览器请求的“负载”,一个键一个键地核对。 - 加密步骤:确认AES加密的次数、每次使用的密钥和IV是否正确。确认RSA加密的原文是否正确(是AES密钥还是随机字符串?)。
- 编码方式:确认每一步的输入输出编码。JS中的字符串是UTF-16还是UTF-8?Python默认是UTF-8。AES加密后,JS是直接输出16进制还是Base64?Python中你做对应的处理了吗?
- RSA公钥:确认你使用的RSA公钥字符串完全正确,包括
-----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----这些标记。有时JS中的公钥是去掉了头尾和换行的,导入时需要自己加上。
- 原始数据构造:检查你的
调试技巧:可以在Python加密函数的关键步骤(如第一次AES加密后、第二次AES加密后、RSA加密后)都打印出中间结果(转为16进制或Base64),与你在JS代码中通过console.log打印的中间结果进行比对,这样可以快速定位哪一步出了问题。
5.2 接口变更与参数更新
网易云音乐的接口和加密逻辑并非一成不变。如果某天发现脚本突然失效,请按以下步骤排查:
- 重新抓包:打开浏览器开发者工具,重新进行一次操作(搜索、播放),观察新的网络请求。
- 检查接口地址:请求的URL是否变了?
- 检查请求参数:
params和encSecKey是否还在?是否有新的参数出现(如header)? - 重新定位JS:使用搜索功能,在加载的JS资源中重新搜索
encSecKey或params,找到新的加密函数。 - 分析新逻辑:重复我们之前的分析过程,更新Python代码中的加密逻辑、密钥或固定参数。
5.3 法律与道德边界
这是一个必须严肃对待的部分。
- 版权保护:通过本技术获取的音频文件,其版权仍归原作者及平台所有。此代码和教程仅限于个人学习、研究爬虫和加密逆向技术之用。
- 禁止商用:绝对不得将获取的内容用于任何商业用途,包括但不限于售卖、在未获授权的平台上传播等。
- 尊重服务器负载:在编写脚本时,应合理设置请求间隔(如使用
time.sleep),避免高频请求对网易云音乐服务器造成不必要的压力,这既是道德要求,也避免你的IP被限制。 - 个人使用界限:下载的内容应限于个人欣赏,不应大规模传播。
提示:在实际操作中,获取到的播放链接(
url)可能不是永久的MP3文件地址,而是有有效期的临时链接,或者是一个需要额外解密的地址。有些高音质格式(如无损)的接口权限更高,加密也更复杂。对于免费用户,可能只能获取到标准或较低音质的链接。这些限制是平台正常的业务逻辑,逆向技术可以帮我们理解过程,但无法绕过所有商业规则。
5.4 代码健壮性优化建议
- 异常处理:如示例代码所示,对网络请求、JSON解析、文件操作等都要进行
try-except包裹,给出友好的错误提示。 - 请求重试:对于网络波动造成的失败,可以加入简单的重试机制。
- 配置化:将加密所需的密钥、接口URL等提取到配置文件(如
config.py)中,方便日后更新。 - 日志记录:使用
logging模块记录程序运行状态、错误信息,便于排查问题。 - 速率限制:在循环下载多首歌曲时,务必在请求间添加延迟,例如
time.sleep(random.uniform(1, 3))。
逆向工程就像解谜,需要耐心、细心和不断的调试。通过这个项目,你不仅能得到一个实用的工具,更能深入理解前端加密、HTTP协议和Python网络编程的许多细节。希望这篇长文能为你打开一扇门,在实际动手解决问题的过程中获得乐趣和成长。如果在复现过程中遇到具体问题,多观察、多比对、多搜索,社区里有很多前辈分享的经验可以借鉴。