1. 项目概述:从一次API调用失败说起

前几天在做一个音乐数据分析的小工具,想批量获取网易云音乐上某个歌单的详细信息,比如歌曲ID、歌名、歌手、专辑这些。我的第一反应当然是去抓它的官方接口。用浏览器开发者工具一抓包,看起来挺简单,一个 POST 请求,参数里带着歌单ID。我兴冲冲地用Python的 requests 库把参数一拼,直接发过去,结果返回来的不是梦寐以求的JSON数据,而是一个冷冰冰的 -460 错误码,意思是“校验失败”。

相信很多做过爬虫或者数据抓取的朋友都遇到过类似的场景。表面上看,请求参数 params data 都是明文的,但服务器就是不认。问题就出在那些我们没有注意到的、被前端JavaScript动态计算出来的加密参数上。在网易云音乐这个案例里,核心就是两个参数: params encSecKey 。它们不是我们手动填写的,而是由网页加载的JavaScript代码,根据我们真实的请求参数(比如歌单ID),经过一系列复杂的加密运算后生成的。服务器收到请求后,会用对应的逻辑解密 params ,并校验 encSecKey ,只有全部通过,才会认为是合法请求并返回数据。

这就是典型的“JS逆向”场景。我们看不到后端源码,但前端代码(JavaScript)是公开的,加密逻辑就藏在里面。我们的目标,就是像侦探一样,从这些混淆过的、压缩过的JS代码中,找到生成 params encSecKey 的完整逻辑,然后用Python等后端语言复现这一过程,从而能够模拟出合法的请求。这不仅仅是“破解”,更是理解现代Web应用前后端安全交互机制的一个绝佳窗口。无论你是想学习爬虫进阶技巧,还是对Web安全感兴趣,或者单纯想解决一个具体的数据获取需求,搞懂网易云音乐的这套加密机制,都是一个非常有价值的实战练习。

2. 核心加密逻辑的逆向分析与拆解

逆向的第一步是定位。在网易云音乐网页版,打开开发者工具(F12),进入Network(网络)面板,清空记录,然后进行一个能触发API请求的操作,比如点击“播放”或者进入一个歌单。很快,你就能在请求列表里找到一个 POST 请求,它的URL通常是 https://music.163.com/weapi/xxx 这种形式。点击这个请求,在 Headers (标头)部分找到 Form Data (表单数据),就能看到我们关心的四个参数: params encSecKey 、还有一个 csrf_token (有时也叫 __csrf )以及 header 。其中, csrf_token 通常可以在页面的Cookie或HTML源码里直接找到,不算核心加密。真正的难点和核心,就是那一长串看起来像乱码的 params encSecKey

那么,生成它们的JavaScript代码在哪里?通常有两个地方:一个是直接内嵌在页面HTML里的 <script> 标签,另一个是外链的JS文件。由于加密是核心功能,网易云音乐将其放在了一个经过混淆和压缩的独立JS文件中。我们可以在Network的JS文件请求里,通过搜索关键字符串来定位。一个非常有效的方法是,在 params encSecKey Form Data 值上右键,选择“Search in all files”(在所有文件中搜索),但注意,加密后的值是每次请求都变化的,直接搜值本身找不到。我们需要搜更底层的、不会变的关键词。

尝试搜索 encSecKey 这个参数名本身,或者搜索 CryptoJS RSA AES 等常见的加密库名。很快,我们就能定位到一个核心的JS文件(不同时期文件名可能不同,如 index.xxxxxx.js )。打开这个文件,它通常是压缩成一行的,我们可以点击开发者工具左下角的 {} (美化代码)按钮,让代码变得可读。

美化之后,代码依然因为变量名被混淆(比如都变成了 a , b , c , d )而难以阅读。但我们可以通过设置断点来动态跟踪。在开发者工具的Sources(源代码)面板,找到这个美化后的JS文件,在疑似加密函数的地方(比如有 return 语句,或者参数名包含 params encSecKey )打上断点。然后回到网页触发一次API请求,代码执行就会暂停在断点处。

这时,我们有两个强大的工具: Call Stack (调用堆栈)和 Scope (作用域)。 Call Stack 可以看到当前函数是被谁调用的,一层层回溯,就能找到加密的入口函数。 Scope 可以看到当前函数作用域内所有变量的值,包括传入的参数、中间计算结果。通过单步调试(F10),观察每一步执行后变量的变化,特别是当执行到某些加密库函数调用(如 CryptoJS.AES.encrypt )时,观察它的输入和输出,我们就能像拼图一样,把整个加密流程还原出来。

经过这样一番调试分析,我们可以梳理出网易云音乐Web API加密的核心逻辑,它主要包含三个关键环节:

  1. AES加密 :我们真实的请求参数(一个JSON字符串,如 {"id":"12345678", "limit": 30} ),首先会被一个随机生成的16字节密钥进行AES加密。这里使用的是AES加密算法中的CBC模式,并且需要提供一个初始化向量(IV)。这个随机密钥和IV,是每次请求都会新生成的,这就保证了每次加密得到的密文(也就是最终的 params )都不同。
  2. RSA加密 :上一步生成的随机AES密钥,并不会直接发送。它会被一个固定的RSA公钥进行加密。RSA是一种非对称加密算法,用公钥加密后,只有持有对应私钥的服务器才能解密。这个加密后的结果,就是 encSecKey 。所以, encSecKey 本质上是保护本次通信所用对称密钥(AES密钥)的“信封”。
  3. Base64编码 :无论是AES加密后的密文( params ),还是RSA加密后的密钥( encSecKey ),都是二进制数据。为了能在HTTP请求中以文本形式传输,它们最后都会进行Base64编码。所以我们看到的那一串“乱码”,其实是Base64编码后的字符串。

注意 :这个逻辑是经过高度概括的。实际逆向中你会发现,网易云音乐的JS代码在AES加密前,可能还会对原始文本进行填充(Padding)以满足块长度要求,并且它使用的AES密钥和IV,可能并非完全随机,而是由一个种子经过特定算法生成。RSA加密也可能不是标准的 RSA/ECB/PKCS1Padding ,而可能有其特定的填充模式。这些细节都需要在调试中逐一确认并记录。

3. 关键加密参数与算法的深度解析

理解了核心流程,我们还需要深挖每一步的具体实现细节,这是用Python复现的前提。下面我们拆解每一个关键部分。

3.1 原始请求文本的构造

在加密之前,我们需要知道要加密的“明文”是什么。以获取歌单详情为例,明文是一个JSON字符串。但这里有个细节:这个JSON字符串的键值对顺序是固定的。在JavaScript中,对象键的顺序在ES6之后虽然有了一定的规则,但为了绝对保险,网易云音乐的代码里通常是按照一个固定的键名列表来拼接字符串,而不是直接 JSON.stringify 一个对象。因为 JSON.stringify 的输出顺序虽然现在多数引擎是按定义顺序,但并非标准保证。服务器在解密后,可能会严格按照它预期的键顺序来解析。顺序不一致可能导致校验失败。

例如,真实的请求参数可能是一个对象:

let requestData = {
    id: '12345678',
    limit: 30,
    offset: 0,
    total: true,
    n: 1000,
    csrf_token: 'your_csrf_token_here' // 注意,这个有时也会被放进明文里一起加密
};

但在加密时,代码可能是这样构造明文的:

let text = `{"id":"${id}","limit":${limit},"offset":${offset},"total":${total},"n":${n},"csrf_token":"${csrf_token}"}`;

或者更常见的是,使用一个固定键名的数组来遍历和拼接。我们在逆向时,必须找到这个构造明文文本的具体函数,确认其顺序和格式。

3.2 AES加密的细节确认

AES加密有几个关键参数必须完全匹配,否则解密端(服务器)无法正确解密。

  • 密钥(Key) :长度必须是16、24或32字节(对应AES-128, AES-192, AES-256)。网易云音乐通常使用AES-128,即16字节密钥。这个密钥是随机生成的,但逆向时我们需要看它是如何生成的。是 window.crypto.getRandomValues 还是 Math.random ?生成的格式是二进制数组( Uint8Array )还是十六进制字符串?这关系到后续RSA加密时对它的处理。
  • 初始化向量(IV) :CBC模式必须的IV,长度是16字节。它同样需要是随机的,并且和密钥的生成方式一致。
  • 填充模式(Padding) :当明文长度不是16字节的倍数时,需要填充。常见的有PKCS#7(也叫PKCS#5)填充。在JavaScript的CryptoJS库中,默认可能就是PKCS#7。我们需要在调试时,观察加密函数传入的参数,或者查看CryptoJS的配置。
  • 输出格式 :AES加密输出的是 CipherParams 对象,我们需要的是其中的 ciphertext 属性,这是一个代表密文的单词数组(WordArray)。最终,这个单词数组会被转换成什么?通常是先转换成 Uint8Array 这样的二进制格式,然后再进行Base64编码。

在调试时,我们可以在AES加密函数执行后,立即在控制台打印出密钥、IV、以及密文单词数组,并记录它们的值。然后自己用Python尝试用相同的密钥、IV、模式和填充去加密同样的明文,看得到的密文是否一致。这是验证我们理解是否正确的最直接方法。

3.3 RSA公钥与加密模式

这是最需要仔细核对的一环。RSA加密的核心是公钥。这个公钥是硬编码在JS文件里的一个很长的字符串(通常是16进制或Base64格式)。我们需要找到它并完整地复制下来。

  • 公钥格式 :这个字符串可能直接就是模数(n),也可能是一个包含模数和指数(e)的完整公钥。在网易云音乐的案例中,常见的是一个非常长的16进制字符串,它代表的就是RSA的模数 n 。指数 e 通常是固定的 010001 (十六进制,即十进制的65537)。所以公钥其实就是 (n, e) 对。
  • 加密模式与填充 :RSA加密不能直接加密原始数据,需要填充。最常见的填充是PKCS#1 v1.5。但在JavaScript中,有时会使用一个叫“无填充”的模式,然后手动在明文前面加上一些特定字节后再进行加密。这需要我们在调试时仔细观察:在调用RSA加密函数(可能是 encrypt ,也可能是 setPublicKey 后调用的某个方法)之前,传入的参数是什么?是原始的AES密钥字符串,还是已经经过某种处理(比如反转字节序)的二进制数据? 一个关键线索是: encSecKey 的长度是固定的256字节(经过Base64编码后是344个字符)。因为RSA-1024加密后的密文长度就是128字节(1024位),Base64编码后就是172字符。但网易云音乐的 encSecKey 长度是344字符,这对应着256字节的原始数据。这说明它可能不是直接用RSA-1024加密的,或者加密前对数据做了扩展。实际上,它使用的是RSA-2048(模长2048位),加密后的数据长度是256字节。而指数 e=65537 是固定的。

在Python中,我们需要使用 rsa 库或 Crypto.PublicKey.RSA 库,用找到的模数 n 和指数 e 构造一个RSA公钥对象,然后使用正确的填充模式(通常是 PKCS1_v1_5 )去加密处理过的AES密钥。

3.4 完整的生成流程串联

将以上所有步骤串联起来,完整的生成流程如下:

  1. 构造明文 :按照固定顺序和格式,将业务参数(如歌单ID、分页参数等)拼接成一个JSON字符串。
  2. 生成随机密钥 :生成一个16字节的随机数据作为AES密钥( aes_key )。
  3. 生成随机IV :生成一个16字节的随机数据作为AES的初始化向量( aes_iv )。
  4. AES加密 :使用 aes_key aes_iv ,以AES-128-CBC模式和PKCS7填充,加密步骤1中的明文,得到二进制密文 aes_encrypted
  5. 处理AES密钥 :将 aes_key (16字节)转换成服务器RSA加密所期望的格式。 这是一个极易出错的点 。在JS中,有时并不是直接加密这16个字节。调试发现,它可能会在这16字节密钥的 前面 填充188个字节的随机数据(或固定值),组成一个长度为204位的序列,然后再进行RSA加密。这样做的目的可能是为了符合RSA PKCS#1 v1.5填充的格式要求,或者是一种自定义的混淆。我们必须通过调试,精确查看被送入RSA加密函数的那个数据到底是什么。假设我们通过调试发现,被加密的数据是 prefix + aes_key ,其中 prefix 是188个字节的随机填充。
  6. RSA加密 :使用固定的RSA公钥( n , e ),对步骤5处理后的数据进行RSA加密(填充模式需与JS端一致,常见为 PKCS1_v1_5 ),得到二进制密文 rsa_encrypted
  7. Base64编码 :将 aes_encrypted 进行Base64编码,得到 params 。将 rsa_encrypted 进行Base64编码,得到 encSecKey
  8. 发起请求 :将 params encSecKey csrf_token 等参数,以 application/x-www-form-urlencoded 格式提交给目标API。

4. 基于Python的完整复现与代码实现

理论清晰后,我们用Python来复现。我们将使用 pycryptodome 库来处理AES和RSA加密,它是 Crypto 库的一个活跃分支。

首先安装依赖:

pip install pycryptodome

接下来是完整的Python实现代码,我们将每一步都封装成函数,并加上详细注释。

import base64
import binascii
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
from typing import Tuple

class NeteaseMusicEncryptor:
    """网易云音乐Web API参数加密生成器"""

    # 固定的RSA公钥 (模数n和指数e)
    # 这个PUBLIC_KEY_STR是经过逆向找到的,通常是16进制字符串
    # 示例值,实际需要从JS中提取
    PUBLIC_KEY_STR = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
    PUBLIC_KEY_EXPONENT = 65537  # 对应的指数e,固定为010001

    # AES相关常量
    AES_KEY_LENGTH = 16  # AES-128
    AES_IV_LENGTH = 16   # CBC模式IV长度
    RSA_ENCRYPTED_PREFIX_LENGTH = 188  # RSA加密前,在AES密钥前填充的随机字节长度

    def __init__(self):
        # 从16进制字符串构造RSA公钥对象
        # 注意:PUBLIC_KEY_STR是16进制,需要转换成整数
        n = int(self.PUBLIC_KEY_STR, 16)
        e = self.PUBLIC_KEY_EXPONENT
        # 构造RSA密钥对象。PyCryptodome的RSA构造需要 (n, e) 对。
        # 我们创建一个自定义的密钥对象。标准方式是使用RSA.construct,但它需要多个参数。
        # 更简单的方式:直接用RSA.import_key导入一个PEM格式的密钥。但这里我们没有PEM。
        # 因此,我们手动构建一个符合PKCS#1的RSAPublicKey结构。
        # 实际上,对于加密操作,我们可以直接使用 (n, e) 来创建公钥。
        # 这里我们使用一个更直接的方法:创建一个假的RSA密钥对象,只包含n和e。
        # 但PyCryptodome的PKCS1_v1_5加密器需要一个完整的RSA密钥对象。
        # 所以我们需要构造一个合法的RSA公钥。
        # 方法:使用RSA.generate生成一个临时密钥,然后替换它的n和e(不推荐,复杂且可能不安全)。
        # 实际上,正确的做法是使用RSA.construct((n, e)),但这要求n和e是整数。
        self.rsa_key = RSA.construct((n, e))
        self.rsa_encryptor = PKCS1_v1_5.new(self.rsa_key)

    def _generate_random_string(self, length: int) -> str:
        """生成指定长度的随机字符串(ASCII字母数字)"""
        return ''.join(random.choices(string.ascii_letters + string.digits, k=length))

    def _generate_random_bytes(self, length: int) -> bytes:
        """生成指定长度的随机字节"""
        return bytes([random.randint(0, 255) for _ in range(length)])

    def build_raw_text(self, data: dict) -> str:
        """
        构造待加密的原始文本。
        注意:键的顺序必须与网易云音乐前端保持一致!
        这里以获取歌单详情(`/weapi/v3/playlist/detail`)为例。
        实际不同接口的参数字段和顺序可能不同,需要根据具体接口调整。
        """
        # 按照观察到的固定顺序排列键
        # 这个顺序是通过逆向JS代码调试得到的,至关重要!
        ordered_keys = ['id', 'limit', 'offset', 'total', 'n', 'csrf_token']
        ordered_dict = {}
        for key in ordered_keys:
            if key in data:
                ordered_dict[key] = data[key]
            else:
                # 如果请求数据中没有某个键,可能需要赋予默认值(如空字符串或0)
                # 但更安全的做法是,只包含请求中实际提供的键,顺序按固定列表来。
                # 这里为了通用性,如果键不存在,我们跳过。
                pass
        # 将字典转换为JSON字符串。json.dumps默认会按键的插入顺序排序,而Python3.7+字典保持插入顺序。
        # 所以我们按ordered_keys的顺序插入,dump出来的顺序就是正确的。
        raw_text = json.dumps(ordered_dict, separators=(',', ':'))  # 移除空格,与JS的JSON.stringify接近
        return raw_text

    def encrypt_params(self, raw_text: str) -> Tuple[str, str]:
        """
        核心加密函数,生成params和encSecKey。
        返回: (params_base64, encSecKey_base64)
        """
        # 1. 生成随机的AES密钥和IV
        aes_key = self._generate_random_bytes(self.AES_KEY_LENGTH)  # 16字节
        aes_iv = self._generate_random_bytes(self.AES_IV_LENGTH)    # 16字节
        # 注意:在实际的网易云音乐JS中,aes_key和aes_iv可能不是完全独立随机,
        # 而是由一个种子生成。但这里我们用完全随机模拟,只要算法一致,服务器能解密即可。

        # 2. AES加密原始文本
        # 使用PKCS7填充 (PyCryptodome中pad函数默认使用PKCS7)
        raw_text_bytes = raw_text.encode('utf-8')
        padded_data = pad(raw_text_bytes, AES.block_size)
        cipher_aes = AES.new(aes_key, AES.MODE_CBC, aes_iv)
        aes_encrypted_bytes = cipher_aes.encrypt(padded_data)

        # 3. 处理AES密钥,准备进行RSA加密
        # 关键步骤:在AES密钥前填充188字节的随机数据
        prefix = self._generate_random_bytes(self.RSA_ENCRYPTED_PREFIX_LENGTH)
        data_to_rsa_encrypt = prefix + aes_key  # 总长度 188 + 16 = 204 字节

        # 4. RSA加密处理后的数据
        # 使用PKCS#1 v1.5填充模式
        rsa_encrypted_bytes = self.rsa_encryptor.encrypt(data_to_rsa_encrypt)
        # 注意:rsa_encryptor.encrypt要求数据长度必须小于密钥长度(256字节)减去填充长度(11字节)。
        # 我们的data_to_rsa_encrypt是204字节,小于245,所以是安全的。

        # 5. Base64编码
        params_base64 = base64.b64encode(aes_encrypted_bytes).decode('utf-8')
        encSecKey_base64 = base64.b64encode(rsa_encrypted_bytes).decode('utf-8')

        return params_base64, encSecKey_base64

    def get_encrypted_params(self, request_data: dict) -> dict:
        """
        对外暴露的主方法。传入业务参数字典,返回包含加密参数的字典,可直接用于requests.post。
        """
        # 1. 构造明文
        raw_text = self.build_raw_text(request_data)
        print(f"[DEBUG] 待加密明文: {raw_text}")  # 调试用

        # 2. 加密得到params和encSecKey
        params, encSecKey = self.encrypt_params(raw_text)

        # 3. 返回完整的表单数据
        # 注意:有些接口还需要csrf_token,它通常来自cookie,需要外部传入。
        # 这里假设request_data里已经包含了csrf_token。
        form_data = {
            'params': params,
            'encSecKey': encSecKey
        }
        # 如果request_data里有csrf_token,也一并放入(有时它不参与加密,直接传)
        if 'csrf_token' in request_data:
            form_data['csrf_token'] = request_data['csrf_token']

        return form_data

# 使用示例
if __name__ == '__main__':
    encryptor = NeteaseMusicEncryptor()

    # 模拟请求歌单详情
    request_data = {
        'id': '123456789',  # 歌单ID
        'limit': 30,
        'offset': 0,
        'total': True,
        'n': 1000,
        'csrf_token': 'your_csrf_token_from_cookie'  # 需要从网页Cookie中获取
    }

    try:
        encrypted_data = encryptor.get_encrypted_params(request_data)
        print("生成的加密参数:")
        print(f"params: {encrypted_data['params'][:50]}...")  # 只打印前50字符
        print(f"encSecKey: {encrypted_data['encSecKey'][:50]}...")
        print(f"csrf_token: {encrypted_data.get('csrf_token', 'N/A')}")

        # 你可以用这个encrypted_data去发起请求
        # import requests
        # url = 'https://music.163.com/weapi/v3/playlist/detail'
        # headers = {
        #     'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...',
        #     'Referer': 'https://music.163.com/',
        #     'Content-Type': 'application/x-www-form-urlencoded'
        # }
        # response = requests.post(url, data=encrypted_data, headers=headers)
        # print(response.json())

    except Exception as e:
        print(f"加密过程出错: {e}")

重要提示 :上面的代码中的 PUBLIC_KEY_STR RSA_ENCRYPTED_PREFIX_LENGTH 是示例值, 它们不是真实的网易云音乐公钥和填充长度 。你必须通过逆向分析,从当前有效的网易云音乐网页JS代码中提取出正确的值。公钥通常是一个很长的16进制字符串。填充长度(188)也是通过调试观察到的,你需要确认在你的逆向环境中, data_to_rsa_encrypt 的长度是否是204字节(188前缀 + 16密钥)。

5. 逆向实战中的调试技巧与避坑指南

理论代码都有了,但在实际逆向和复现过程中,你会遇到无数个坑。下面分享一些血泪换来的经验。

5.1 如何精准定位加密函数?

  1. XHR断点 :在开发者工具的Sources面板,点击右侧的“XHR/fetch Breakpoints”,添加一个包含“weapi”的URL断点。这样任何向包含“weapi”的地址发起的请求都会暂停,你可以直接跳到发送请求的JavaScript代码处,然后顺着调用栈往上找。
  2. 搜索特征字符串 :除了搜索 encSecKey ,还可以搜索 CryptoJS mode: CBC padding: PKCS7 setPublicKey encrypt 等。有时加密函数会被赋值给一个全局变量,搜索 window.asrsea window.xxx (一个很短的函数名)也可能有奇效。
  3. Hook关键函数 :在Console面板,你可以重写标准的Web API或加密函数,来拦截参数。例如:
    var _original_encode = window.btoa; // Hook Base64编码
    window.btoa = function(data) {
        console.trace('btoa called with:', data);
        return _original_encode(data);
    };
    
    或者直接Hook XMLHttpRequest send 方法,查看每次发送的数据。这是一种非常强大的动态分析手段。

5.2 加密结果不一致的排查思路

当你用Python复现的 params encSecKey 与浏览器生成的不一样时,按以下顺序排查:

  1. 明文是否完全一致? 这是最常见的问题。确保你的JSON字符串和JavaScript生成的 一模一样 ,包括所有键值对、顺序、布尔值( true / false 是小写)、没有多余的空格。最好在JS加密前,用 console.log 打印出明文字符串,然后复制到Python里作为输入。
  2. AES密钥和IV是否一致? 在JS加密代码中,在生成 aes_key aes_iv 后立刻打印它们的值(可能是16进制或Base64格式)。然后在Python中,在加密前也打印出来。确保它们是完全相同的字节序列。注意,JS的随机生成函数(如 Math.random )和Python的 random 模块在算法上不同, 你不能指望生成一样的随机数 。我们的目标不是生成相同的密钥,而是理解密钥的 生成逻辑 。如果JS里密钥是来自一个固定种子或特定算法,你必须在Python里复现这个算法,而不是简单地调用 random
  3. AES参数是否正确? 确认模式(CBC)、填充(PKCS7)、密钥长度(128)、IV长度(16)全部匹配。在PyCryptodome中, AES.new(key, AES.MODE_CBC, iv) 默认就是PKCS7填充。但JS的CryptoJS可能默认是PKCS5(对于AES来说和PKCS7等价)。仍需确认。
  4. RSA加密前的数据处理是否正确? 这是第二大坑。你必须百分之百确认,在JS中,被送入RSA加密函数的数据到底是什么。是单纯的16字节 aes_key 吗?还是 aes_key 反转了字节序?还是在前面加了188字节的随机数?一定要在JS调试中,把那个即将被RSA加密的变量(可能是一个 ArrayBuffer Uint8Array )的内容以16进制形式打印出来,然后在Python里构造一个一模一样的数据去加密。
  5. RSA公钥和填充模式是否正确? 确认你使用的模数 n 和指数 e 完全正确。确认Python中使用的填充模式( PKCS1_v1_5 )与JS端一致。有些JS库可能会使用“无填充”( NoPadding ),然后自己手动进行填充,这需要你同样在Python中手动实现填充逻辑。

5.3 处理JS代码混淆与反调试

网易云音乐的JS代码是高度混淆的,变量名都是 a,b,c ,并且可能加入了反调试手段。

  • 反调试 :有些代码会检测开发者工具是否打开,如果打开就进入死循环或报错。你可以通过设置“停用断点”(Deactivate breakpoints)或使用“条件断点”来绕过。或者直接找到检测代码并修改它(在Sources面板编辑JS文件,但刷新后失效)。
  • 代码美化与重命名 :利用开发者工具的美化功能后,可以尝试手动给一些关键变量或函数重命名,帮助你理解逻辑。虽然刷新就没了,但对单次分析很有帮助。
  • 关注核心逻辑,忽略无关代码 :不要试图理解所有代码。聚焦在生成 params encSecKey 的函数调用链上。对于复杂的工具函数,只要知道它的输入输出即可,不必深究其内部实现。

5.4 不同接口的差异化处理

网易云音乐有众多API接口(如搜索、获取歌曲详情、获取评论、用户登录等)。它们的加密核心逻辑(AES+RSA)是相同的,但有以下可能的变化点:

  • 明文结构不同 :每个接口需要加密的业务参数不同,键的顺序也可能不同。你必须为每个需要爬取的接口单独分析其明文构造逻辑。
  • 加密函数可能不同 :虽然核心是同一个,但可能会被包装成不同的函数名,或者传入一些额外的配置参数。你需要找到对应接口调用的那个加密入口。
  • 是否需要 csrf_token :大部分接口需要,且需要从Cookie中获取 __csrf 字段的值,并放入明文中或作为单独参数。登录等敏感接口可能有额外的校验。

一个实用的建议 :不要试图写一个通用的、覆盖所有接口的加密函数。而是为每个主要的接口写一个专门的“参数构造器”,它负责按照该接口的规则构造明文字典,然后调用统一的 encrypt_params 核心加密函数。这样代码更清晰,也更容易维护。

6. 常见问题与解决方案速查表

在实际操作中,你几乎一定会遇到下面这些问题。这里提供一个快速排查指南。

问题现象 可能原因 解决方案
请求返回 -460 (校验失败) 1. params encSecKey 生成错误。
2. 明文JSON键顺序错误。
3. csrf_token 缺失或错误。
4. 请求头(如 User-Agent , Referer )不正确。
1. 用浏览器生成一次正确的参数,与你Python生成的进行逐字节对比。
2. 确保明文构造顺序与JS完全一致。
3. 检查Cookie中是否有 __csrf ,并正确传入。
4. 模拟浏览器,添加完整的请求头。
请求返回 -2 (参数错误) 明文数据格式错误,或缺少必需参数。 检查明文JSON是否符合接口要求,所有必需字段是否都已包含。
Python RSA加密时报错: Data too large for key size. 待RSA加密的数据长度超过了密钥长度减去填充长度的最大值。 确认RSA加密前的数据长度。对于2048位密钥,PKCS1_v1_5填充下,最大加密数据长度为 256 - 11 = 245字节。检查你的 prefix + aes_key 总长度是否超过245。网易云音乐的204字节是安全的。
Python AES加密结果与JS不同 1. 密钥/IV不同。
2. 填充模式不同。
3. 明文编码不同。
1. 确保密钥和IV的字节序列完全一致。
2. 确认JS使用的是PKCS7填充。
3. 确保明文字符串编码为UTF-8。
无法在JS中找到加密函数 代码混淆严重,或加密逻辑被隐藏。 1. 尝试搜索 encrypt CryptoJS RSA
2. 使用XHR断点直接定位到发起网络请求的代码行。
3. 在可能初始化加密模块的地方下断点。
JS调试时,变量值显示为 undefined 或被优化 代码被V8引擎优化,变量不可见。 1. 在开发者工具设置中关闭“启用JavaScript源映射”。
2. 在函数开头或可能未被优化的地方下断点。
3. 使用 console.log 在代码中直接打印变量值。
生成的 encSecKey 长度不是344字符 RSA加密后的字节数不是256。 确认RSA密钥是2048位。确认加密后的数据进行了正确的Base64编码(没有换行符)。

最后,也是最关键的一点: 网易云音乐的加密机制不是一成不变的 。随着前端代码的更新,公钥、加密流程、参数构造方式都有可能发生变化。今天有效的代码,明天可能就失效了。因此,掌握逆向分析的 方法 远比记住一套具体的参数和代码更重要。当你的爬虫失效时,你需要有能力重新打开开发者工具,定位新的加密逻辑,并调整你的Python代码。这个过程本身就是对Web安全和技术细节的深刻学习,其价值远超单纯获取数据本身。

更多推荐