前言
在对网易云音乐相关接口进行逆向分析时,可以发现其请求参数并不是明文提交,而是经过一套固定的前端加密流程处理后,再发送给服务端。
这类场景在实际项目中非常常见:前端通过 JavaScript 对请求参数进行加密,服务端再按照既定规则解密,从而完成接口通信。本文的重点不放在 RSA 底层实现细节上,而是聚焦于这条参数加密链路本身,梳理其整体流程、关键函数职责,以及最终参数 params 与 encSecKey 的生成方式。
从整体上看,这套逻辑可以概括为:
请求参数经过两次 AES-CBC 加密,第二层 AES 的随机密钥再通过 RSA 公钥加密,最终生成
params与encSecKey两个字段。
一、整体加密流程概览
在具体分析代码之前,可以先从整体上理解这套参数生成逻辑。

也就是说,前端最终提交的并不是原始业务参数,而是两部分内容:
params:业务参数经过双层 AES 加密后的结果encSecKey:第二层 AES 随机密钥经过 RSA 公钥加密后的结果
二、参数生成入口分析
在逆向得到的 JavaScript 代码中,参数生成通常会有一个统一的入口函数。这个函数本身并不负责所有底层细节,而是负责准备加密所需的固定参数,并调用核心处理逻辑。
从代码结构来看,入口函数一般会准备以下几个关键值:
第一层 AES 使用的固定 key
RSA 所需的公钥指数
RSA 所需的模数
原始待加密请求参数
然后再将这些内容传入真正执行加密的核心函数中。
因此,入口函数的职责可以概括为:
准备固定参数,组织调用流程,并返回最终加密结果。
下面这段截图和代码就是参数生成入口的部分:



// 调用 asrsea 对请求参数进行加密:
// 第一个参数是原始 JSON 字符串
// 后面几个参数分别对应 RSA 公钥指数、RSA 模数和固定 AES key
var bWo5t = window.asrsea(
JSON.stringify(i6c),
bod3x(["流泪", "强"]),
bod3x(AY2x.md),
bod3x(["爱心", "女孩", "惊恐", "大笑"])
);从调用关系来看,真正的加密逻辑并不直接写在这一行里,而是封装在 asrsea 对应的下层函数中。
三、核心流程分析:双层 AES + RSA
真正的重点在于核心函数内部的处理过程。整个加密流程可以拆成以下几个步骤。
1. 生成第二层 AES 所需的随机 key
代码执行时,会先生成一个长度为 16 的随机字符串,并将其作为第二层 AES 加密使用的 key。
这一点非常关键,因为它说明:
第一层 AES 使用固定 key
第二层 AES 使用随机生成的 key
也正因为第二层 key 是随机生成的,所以即使原始请求参数相同,不同请求生成的最终密文通常也不会相同。
这一步可以理解为:
为第二层 AES 提供随机密钥,从而使相同请求在不同次发送时生成不同的密文结果。
2. 第一次 AES 加密:固定 key
原始请求参数进入加密流程后,会先进行一次 AES 加密,这一次使用的是固定 key。
因此,第一层 AES 更像是前后端约定好的固定加密步骤。无论提交什么业务参数,都会先经过这一层处理。
从职责上看,这一步完成的是:
将原始业务参数转换为第一层密文。
3. 第二次 AES 加密:随机 key
在第一次 AES 加密完成之后,代码并不会直接提交结果,而是继续使用前面生成的 16 位随机 key,对第一层密文再做一次 AES 加密。
因此,最终的 params 并不是原始参数经过一次加密后的结果,而是第一次 AES 输出结果再经过第二次 AES 处理后得到的密文。
这一点也是分析过程中最需要讲清楚的地方。相比简单地说“参数进行了 AES 加密”,更准确的说法应该是:
请求参数经过了双层 AES-CBC 加密。
下面这段截图和代码可以直接体现出整个核心流程:




function d(d, e, f, g) {
var h = {},
i = a(16); // 随机生成 16 位字符串,作为第二层 AES 的 key
return h.encText = b(d, g), // 使用固定 key g 对原始参数 d 进行第一次 AES 加密
h.encText = b(h.encText, i), // 使用随机 key i 对第一次 AES 的结果进行第二次 AES 加密
h.encSecKey = c(i, e, f), // 使用 RSA 公钥参数对随机 key i 进行加密,生成 encSecKey
hhttps://fengxiaoyao-halo-1327784924.cos.ap-guangzhou.myqcloud.com/halo/PixPin_2026-03-08_17-45-13.png
}从这段代码可以看出:
b(d, g)对原始参数进行第一次 AES 加密b(h.encText, i)对第一次加密结果进行第二次 AES 加密c(i, e, f)则负责使用 RSA 处理第二层 AES 的随机 key
4. RSA 的作用:加密第二层 AES 的随机 key
在完成双层 AES 之后,前端还需要将第二层 AES 使用的随机 key 安全地传给服务端,否则服务端就无法还原 params。
因此,这个随机 key 不会明文发送,而是会再经过 RSA 公钥加密,生成另一个参数,一般就是 encSecKey。
这里很容易出现一个理解误区:很多人在第一次分析时,会直接把它概括成“RSA 加密了请求参数”。但从实际代码逻辑来看,RSA 并不是直接对原始业务参数进行加密,而是负责:
加密第二层 AES 的随机密钥。
也就是说:
双层 AES 负责保护业务数据
RSA 负责保护对称加密所使用的随机 key
这才是这套加密流程更准确的理解方式。
四、AES 实现部分的关键信息
在实际分析中,AES 部分通常比 RSA 更容易直接看清,因为调用关系和参数传递相对直观。
从代码中可以提炼出以下几个重点。
1. 使用的是 AES-CBC 模式
在 AES 封装函数中,可以看到其调用方式通常类似于 CryptoJS.AES.encrypt(...),并显式指定了加密模式为 CBC。
因此可以确认:
这里使用的是 AES-CBC,而不是 ECB。
CBC 模式需要额外传入 IV,这也是后续分析中的一个重要点。
2. IV 是固定值
从代码中可以看到,AES 使用的 IV 并不是动态生成的,而是一个固定字符串。
也就是说,这里的 IV 由前端和服务端共同约定,并作为协议实现的一部分被写死在代码中。
这一点在分析时可以总结为:
两层 AES 均使用固定 IV,IV 并不参与随机生成。
3. 第一层 key 固定,第二层 key 随机
这是整套逻辑中最值得单独强调的一点:
第一层 AES 的 key 是固定写死的
第二层 AES 的 key 是运行时随机生成的 16 位字符串
所以从职责上看:
第一层更像是协议层固定处理
第二层则让每次请求的密文呈现出动态变化

五、最终参数 params 与 encSecKey 的含义
当整个流程执行完之后,前端最终提交给服务端的核心参数通常就是:
paramsencSecKey
它们分别承担不同职责。
params
params 是原始业务参数经过双层 AES-CBC 加密后的结果,它承载的是业务数据本身的密文。
encSecKey
encSecKey 是第二层 AES 随机 key 经过 RSA 公钥加密后的结果,它承载的是用于解开第二层 AES 的随机密钥密文。
因此,这两个字段的职责可以总结为:
params用于传输业务数据密文,encSecKey用于传输随机对称密钥的密文。
在实际调用中,也能看到它们最终是如何被组装进请求参数中的:
var bWo5t = window.asrsea(
JSON.stringify(i6c),
bod3x(["流泪", "强"]),
bod3x(AY2x.md),
bod3x(["爱心", "女孩", "惊恐", "大笑"])
);
e6c.data = j6d.cq6k({
params: bWo5t.encText, // 将加密结果 encText 赋值给请求参数 params
encSecKey: bWo5t.encSecKey
});这也进一步说明:
encText最终对应的是请求里的paramsencSecKey则作为另一部分密钥参数一起提交
六、Python 复现加密链路
在梳理清楚 JavaScript 中的参数生成逻辑后,就可以通过 Python 对这段前端逻辑进行复现。
常见实现方式如下:
读取本地 JS 文件
使用
execjs编译 JS 代码将待加密 JSON 参数传入入口函数
获取加密后的
params和encSecKey将这两个参数作为 POST 数据提交给接口
通过这一步,可以验证逆向分析并不仅停留在“看懂流程”,而是已经能够实际生成服务端需要的参数,完成接口请求复现。
从逆向实践的角度看,这一步的意义在于:
将前端参数构造逻辑迁移到本地脚本中,实现自动化调用。
下面是一份精简后的 Python 复现代码:
import json
import requests
import execjs
# 读取逆向得到的 JS 加密逻辑
def read_js(js_path="wangyiyun_jiami.js"):
with open(js_path, "r", encoding="utf-8") as f:
return f.read()
# 调用 JS 中的入口函数,生成 params 和 encSecKey
def get_encrypted_params(song_id: int, csrf_token: str = ""):
payload = {
"csrf_token": csrf_token,
"encodeType": "aac",
"ids": [song_id],
"level": "exhigh"
}
# 序列化为紧凑 JSON,避免多余空格影响结果
payload_str = json.dumps(payload, separators=(",", ":"))
js_code = read_js()
ctx = execjs.compile(js_code)
# 调用 JS 中的入口函数 fn,生成加密参数
result = ctx.call("fn", payload_str)
return {
"params": result["encText"],
"encSecKey": result["encSecKey"]
}
# 发送请求,获取接口返回结果
def request_song_url(song_id: int, csrf_token: str = ""):
url = "https://music.163.com/weapi/song/enhance/player/url/v1"
headers = {
"User-Agent": "Mozilla/5.0",
"Referer": "https://music.163.com/",
"Origin": "https://music.163.com"
}
# 如果接口校验登录态,这里需要补充对应的 cookie 信息
cookies = {
# "MUSIC_U": "你的登录态",
# "__csrf": "你的 csrf_token"
}
encrypted_data = get_encrypted_params(song_id, csrf_token)
params = {
"csrf_token": csrf_token
}
response = requests.post(
url=url,
params=params,
data=encrypted_data,
headers=headers,
cookies=cookies
)
response.raise_for_status()
return response.json()
def main():
song_id = 3352826363
result = request_song_url(song_id, csrf_token="")
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()需要注意的是,实际请求中如果接口依赖登录态,还需要补充对应的 cookie 或 token 信息。出于安全考虑,示例代码中对这部分内容做了省略处理。
七、总结
通过对这段前端加密逻辑的分析,可以将整个参数生成过程清晰地拆分为三部分:
原始请求参数先经过固定 key 的 AES-CBC 加密
第一层密文再经过随机 key 的第二次 AES-CBC 加密
第二层 AES 的随机 key 再通过 RSA 公钥加密生成
encSecKey
最终,前端提交给服务端的并不是原始业务参数,而是:
双层 AES 加密后的
paramsRSA 加密随机 key 后的
encSecKey
从逆向分析角度看,这种方案本质上属于一种典型的混合加密设计:
对称加密负责保护业务数据
非对称加密负责保护对称密钥
理解清楚这条链路之后,不仅有助于复现具体接口请求,也能帮助后续分析其他类似前端加密场景时更快建立整体思路。