1. 项目概述:从一次失败的请求说起
那天,我像往常一样,想写个小脚本把我网易云音乐歌单里的歌名和歌手信息扒下来,做个本地备份。用开发者工具一看,一个简单的搜索请求,
POST
数据里赫然躺着三个让人头大的参数:
csrf_token
、
params
和
encSecKey
。直接复制
curl
命令?秒速返回
-460
错误码。这就是网易云音乐 Web 端核心的加密防线,也是无数爬虫爱好者和前端安全研究者绕不开的“必修课”。这三个参数,特别是后两者,构成了一个典型的非对称与对称加密混合的客户端加密方案,目的很明确:防止请求被轻易模拟和篡改,保护核心业务接口。
如果你也遇到过类似问题,或者对前端
JavaScript
逆向、加密算法实战感兴趣,那么这次对网易云音乐登录、搜索、评论等核心接口的
csrf
、
params
、
encSecKey
算法逆向详解,就是为你准备的。我将带你深入
JavaScript
代码丛林,一步步定位关键加密函数,还原其完整的生成逻辑。这不仅是一次爬虫技巧的分享,更是一次对现代 Web 应用前端安全机制的深度剖析。无论你是想理解其原理,还是需要在自己的项目中实现模拟请求,这篇文章都将提供清晰的路径和可复现的代码。
2. 核心加密机制总览与逆向思路
在动手逆向之前,我们必须先理解我们要破解的究竟是什么。网易云音乐 Web 端(
music.163.com
)对关键接口的请求体进行了整体加密,其结构通常如下:
{
"params": "C7B...(很长的一段密文)",
"encSecKey": "84B...(很长的一段密文)",
"csrf_token": "1234567890abcdef"
}
这三个参数各有其职:
-
csrf_token: 相对简单,通常与用户会话绑定,可以从 Cookie 或页面 HTML 中提取,主要用于防御跨站请求伪造。它的逆向不是重点,重点是获取它的方式。 -
params: 这是核心的业务参数经过加密后的结果。比如你搜索“周杰伦”,那么{“s”: “周杰伦”, “type”: 1}这个对象就会被加密成params的值。它使用的是 AES-128-CBC 对称加密算法。 -
encSecKey: 这是解密params所需密钥的密文。因为 AES 加密需要密钥,但这个密钥不能明文传输。所以网易云采用 RSA 非对称加密,将 AES 密钥用公钥加密后,作为encSecKey发送。服务器用私钥解密encSecKey得到 AES 密钥,再用这个 AES 密钥解密params得到原始业务参数。
所以,整个流程是:
客户端生成一个随机的 AES 密钥 -> 用这个 AES 密钥加密业务参数得到
params
-> 用固定的 RSA 公钥加密这个 AES 密钥得到
encSecKey
-> 将
params
和
encSecKey
发送给服务器。
我们的逆向目标,就是在
JavaScript
代码中找到:
生成随机 AES 密钥的函数、执行 AES 加密的函数、以及执行 RSA 加密的函数和那个固定的公钥。
注意 :网易云音乐的加密逻辑可能会随着版本更新而改变,本文分析的逻辑基于一个较长时期内稳定的版本。逆向的核心思路是通用的,但具体函数名和密钥需要你根据当时的实际情况进行定位。
2.1 逆向工具与前期准备
工欲善其事,必先利其器。进行
JS
逆向,你需要以下工具:
- 浏览器开发者工具 :Chrome 或 Edge 的 DevTools 是主战场。重点关注 Sources (源码)和 Network (网络)面板。
-
代码搜索与格式化
:在 Sources 面板中,对于混淆的、压缩成一行的
JS文件,可以使用{}(Pretty-print)按钮进行格式化,让代码可读。 -
断点调试
:这是逆向的灵魂。通过
XHR/fetch Breakpoints
(在 Sources 面板的 XHR/fetch Breakpoints 区域添加包含特定 URL 关键词的断点),可以在发起目标网络请求时自动暂停
JavaScript执行,从而定位到加密发生的代码位置。 - Node.js 环境 :用于在本地运行和测试我们还原出来的加密算法。确保已安装 Node.js。
逆向心法
:不要试图去理解每一行混淆后的代码。我们的策略是“顺藤摸瓜”,从网络请求的发起处(如
fetch
或
XMLHttpRequest.send
)设置断点,一步步回溯,找到加密参数被赋值的地方,再向上追溯加密函数的调用。
3. 定位关键加密函数与参数生成逻辑
打开网易云音乐网页,进入任意页面(如首页)。打开开发者工具的 Network 面板,勾选
Preserve log
,然后进行一次搜索操作。
在 Network 面板中,找到类型为
xhr
或
fetch
的请求,其请求 URL 可能包含
weapi
字样(如
/weapi/search/get
)。点击这个请求,查看其
Headers
和
Payload
。确认它的请求体正是我们关注的
params
,
encSecKey
,
csrf_token
格式。
接下来是关键步骤:
-
设置全局断点
:在 Sources 面板,找到
XHR/fetch Breakpoints
,点击
+号,添加一个包含weapi关键词的断点。这样,任何发起包含weapi的请求的代码执行时,都会自动暂停。 -
触发断点
:在网页上再次执行搜索操作。此时
JavaScript执行会立即暂停。 -
调用栈分析
:查看右侧的
Call Stack
(调用栈)。这里显示了从断点处往回的函数调用链。我们需要在调用栈中寻找看起来与加密相关的函数名,或者寻找我们的目标参数(
params,encSecKey)被赋值的时刻。 -
回溯与搜索
:在调用栈中,逐个点击上层的函数,观察其代码。通常,你会看到一个函数,它接收一个明文对象(如
{s: “周杰伦”}),然后经过一些处理,最终生成了params和encSecKey。这个函数很可能被命名为window.asrsea或类似的名字(经过混淆,名字可能不同,但逻辑一致)。
另一种高效的方法是直接在全站加载的
JS
文件中搜索关键词。在 Sources 面板中,按
Ctrl+Shift+F
进行全局搜索。可以尝试搜索
encSecKey
、
params
、
AES
、
RSA
、
CBC
等。通常,你会找到一个核心的、被大量引用的加密函数定义。
以我最近一次分析为例,核心加密函数是一个名为
window.asrsea
的函数(函数名可能变化,但功能一致)。它的结构非常清晰:
function d(a, b, c, e) { // 这个函数往往被命名为 asrsea
// a: 明文业务参数 (JSON字符串)
// b: 固定值,与加密算法有关
// c: 固定值,通常是 RSA 公钥的模数 (n)
// e: 固定值,通常是 RSA 公钥的指数 (e)
var f, g, h = {}, i = []; // i 是填充数组
// ... 省略部分混淆代码 ...
f = e(a); // e 是一个函数,用于生成16字节随机字符串作为 AES 密钥
g = d(f, b); // d 是一个函数,用 AES-CBC 加密数据a,密钥为f,得到params
h.encText = g; // 这就是 params
h.encText = b(g, f); // 注意:这里可能有混淆,实际是 h.encSecKey = ...
// 更常见的逻辑是:
// h.encText = g; // params
// h.encSecKey = r(f, c, e); // r 是一个函数,用 RSA 公钥(c, e)加密 AES 密钥(f)
return h;
}
通过断点调试,我们可以确认传入这个函数的四个参数
(a, b, c, e)
中:
-
a是我们要加密的业务数据(JSON 字符串)。 -
b是一个固定字符串,如"010001"。 -
c是一个很长的固定16进制字符串,即 RSA 公钥的模数n。 -
e是一个固定字符串,如"00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"。
而函数内部,
e(a)
(注意这个
e
是传入的第四个参数,与函数内的变量
e
不同,这是混淆的常见手法)这个调用返回了一个16字节的随机字符串,它就是 AES 密钥。随后,这个密钥被用于加密
a
得到
params
,其自身又被 RSA 公钥
(c, b)
加密得到
encSecKey
。
4. 算法还原与本地 Node.js 实现
定位到核心函数后,我们需要将其逻辑用清晰的、可维护的 Node.js 代码还原出来。关键在于识别出内部的 AES 和 RSA 函数,并找到它们对应的标准库实现。
4.1 提取关键常量与函数
首先,我们从
JS
代码中提取出固定的常量。这些通常是写死在代码里的:
-
RSA 公钥的模数
n(对应参数c) :一个很长的16进制字符串。 -
RSA 公钥的指数
e(对应参数b) :通常是"010001"(十进制 65537,这是 RSA 最常用的公钥指数)。 -
AES 加密的初始向量
iv:通常是"0102030405060708"的16进制表示,或者全零"0000000000000000",需要在代码中确认。 -
AES 加密模式与填充
:确认是
AES-128-CBC模式,填充方式为PKCS#7。
4.2 还原 AES 加密逻辑
在核心函数内部,用于加密业务参数
a
的函数(上面伪代码中的
d(f, b)
,但实际参数顺序可能不同)就是 AES 加密。在 Node.js 中,我们可以使用
crypto
标准库来实现。
关键点 :
- 密钥 :是上面生成的16字节随机字符串。
-
明文
:需要加密的原始业务参数字符串(如
{"s":"周杰伦","type":1})。 -
模式
:
aes-128-cbc。 - IV :固定值,需要从源码中确认。
-
输入输出编码
:
JS端处理的是二进制数据,但常以16进制字符串形式传递。我们需要确保在 Node.js 中,密钥、IV、明文都以Buffer类型处理。
一个典型的 AES 加密函数还原如下:
const crypto = require('crypto');
function aesEncrypt(text, key, iv) {
// text: 明文字符串
// key: 16字节的密钥字符串
// iv: 16字节的初始化向量字符串
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
// 网易云音乐通常会将结果转换为大写,需要确认
return encrypted.toUpperCase();
}
4.3 还原 RSA 加密逻辑
用于加密 AES 密钥的函数是 RSA 加密。在
JS
源码中,它可能是一个自定义的大整数运算函数,因为
n
非常大(2048位或以上),超出了
JavaScript
原生整数的表示范围。在 Node.js 中,我们同样可以使用
crypto
库,或者使用
node-rsa
等库。但为了精确还原,我们需要实现和
JS
端一模一样的“教科书式” RSA 加密。
网易云音乐使用的 RSA 加密是
“无填充”
的。这意味着它直接计算
(密钥 ^ e) mod n
。由于密钥是16字节(128位),而
n
是2048位,所以直接计算是可行的。
我们可以使用
BigInt
类型来处理大整数运算:
function rsaEncrypt(key, pubKeyN, pubKeyE) {
// key: 16字节的随机字符串(AES密钥)
// pubKeyN: RSA 模数 n (16进制字符串)
// pubKeyE: RSA 指数 e (16进制字符串),通常是 "010001"
// 1. 将 AES 密钥(文本)反转(这是网易云的一个特定步骤,务必确认!)
// 例如,在 JS 源码中可能看到:key = key.split('').reverse().join('');
const reversedKey = key.split('').reverse().join('');
// 2. 将反转后的密钥转换为16进制
const hexKey = Buffer.from(reversedKey, 'utf8').toString('hex');
// 3. 进行 RSA 加密: (text ^ e) mod n
// 这里 text 是 hexKey 表示的 BigInt
const biText = BigInt('0x' + hexKey);
const biE = BigInt('0x' + pubKeyE);
const biN = BigInt('0x' + pubKeyN);
const biResult = biText ** biE % biN; // 使用 BigInt 运算
// 4. 将结果转换为16进制字符串,并填充至指定长度(通常是256字节,512个16进制字符)
let result = biResult.toString(16);
// 补前导零
while (result.length < 512) {
result = '0' + result;
}
return result;
}
实操心得 :
key的反转操作是网易云音乐加密中的一个经典特征,也是早期很多逆向分析容易忽略导致失败的点。务必通过断点调试,对比加密前的key和传入 RSA 函数时的key,确认是否有此步骤。此外,最终encSecKey的长度是固定的(512位16进制字符),如果长度不对,服务器会拒绝请求。
4.4 整合与 csrf_token 获取
最后,我们将所有步骤整合,并解决
csrf_token
的问题。
const crypto = require('crypto');
// 从 JS 源码中提取的固定值(示例,需根据实际情况替换)
const PUB_KEY_N = ‘00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7’;
const PUB_KEY_E = ‘010001’;
const AES_IV = ‘0102030405060708’; // 也可能是 ‘0000000000000000’
function getRandomString(length) {
// 生成指定长度的随机字符串,作为 AES 密钥
const chars = ‘abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789’;
let result = ‘’;
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
function aesEncrypt(text, key, iv = AES_IV) {
const cipher = crypto.createCipheriv(‘aes-128-cbc’, Buffer.from(key, ‘utf8’), Buffer.from(iv, ‘utf8’));
let encrypted = cipher.update(text, ‘utf8’, ‘hex’);
encrypted += cipher.final(‘hex’);
return encrypted.toUpperCase();
}
function rsaEncrypt(key, n = PUB_KEY_N, e = PUB_KEY_E) {
// 关键:反转密钥
const reversedKey = key.split(‘’).reverse().join(‘’);
const hexKey = Buffer.from(reversedKey, ‘utf8’).toString(‘hex’);
const biText = BigInt(‘0x’ + hexKey);
const biE = BigInt(‘0x’ + e);
const biN = BigInt(‘0x’ + n);
const biResult = biText ** biE % biN;
let result = biResult.toString(16);
while (result.length < 512) {
result = ‘0’ + result;
}
return result;
}
function generateEncryptedParams(plainData) {
// plainData: 明文的业务参数对象,如 {s: ‘周杰伦’, type: 1, limit: 30, offset: 0}
const text = JSON.stringify(plainData);
const aesKey = getRandomString(16); // 生成16位随机 AES 密钥
const params = aesEncrypt(text, aesKey); // 加密业务数据
const encSecKey = rsaEncrypt(aesKey); // 加密 AES 密钥
return { params, encSecKey };
}
// 获取 csrf_token,通常来自 Cookie 中的 ‘__csrf’ 字段
// 在模拟请求时,需要先访问页面获取 Cookie,或从已有会话中提取
function getCsrfTokenFromCookie(cookieString) {
const match = cookieString.match(/__csrf=([^;]+)/);
return match ? match[1] : ‘’;
}
// 示例:模拟搜索请求
async function simulateSearch(keyword) {
// 1. 先获取初始 Cookie 和 csrf_token (这里需要模拟一次页面请求)
// const { cookie, csrf } = await fetchInitialPage();
// 2. 构造明文参数
const plainData = {
s: keyword,
type: 1, // 1: 单曲
limit: 30,
offset: 0
};
// 3. 生成加密参数
const encrypted = generateEncryptedParams(plainData);
// 4. 构造最终请求载荷
const payload = {
params: encrypted.params,
encSecKey: encrypted.encSecKey,
csrf_token: ‘你的csrf_token’, // 替换为实际获取的值
};
console.log(‘加密后的参数:’, payload);
// 5. 使用 axios, node-fetch 等库发送 POST 请求
// const response = await fetch(‘https://music.163.com/weapi/search/get’, {
// method: ‘POST’,
// headers: { ‘Content-Type’: ‘application/x-www-form-urlencoded’ },
// body: new URLSearchParams(payload).toString(),
// cookies: …
// });
}
5. 常见问题、调试技巧与进阶思考
即使按照上述步骤还原了代码,在实际运行中也可能遇到各种问题。这里记录一些常见的坑和调试技巧。
5.1 常见问题排查表
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
返回
-460
错误码
| 加密参数不正确,服务器解密失败。 |
1. 核对
PUB_KEY_N
,
PUB_KEY_E
,
AES_IV
是否与当前网页源码一致。
2. 重点检查 AES 密钥是否进行了反转 。在
rsaEncrypt
函数开始处打印
key
和
reversedKey
,与浏览器中断点处的值对比。
3. 检查
params
加密前的明文是否与浏览器完全一致(包括空格、引号格式)。建议将浏览器中的明文直接复制到你的代码中测试。
|
返回
-2
错误码
|
通常意味着
csrf_token
无效或缺失。
|
1. 确认请求头中携带了正确的
Cookie
。
2. 确认
csrf_token
是从当前会话的 Cookie (
__csrf
) 中正确提取的。
3. 尝试先访问一次主页或登录页,获取有效的 Cookie 和
csrf_token
后再发起请求。
|
encSecKey
长度不是512位
| RSA 加密结果未补零。 |
确保
rsaEncrypt
函数最后有补零到512个16进制字符(256字节)的逻辑。
|
params
加密结果与浏览器不一致
| AES 加密模式、填充或 IV 错误。 |
1. 确认使用
aes-128-cbc
和
PKCS7
填充。
2. 确认
IV
是
Buffer.from(‘0102030405060708’, ‘utf8’)
还是
Buffer.from(‘0102030405060708’, ‘hex’)
?
必须和 JS 端保持一致
。通常 JS 里
IV
是字符串
’0102030405060708’
,在
createCipheriv
时需要转换为
Buffer
。用
‘utf8’
还是
‘hex’
编码,需要看 JS 源码如何处理。断点查看
CryptoJS.enc.Utf8.parse(iv)
或类似语句,可以确定。
|
| 请求成功但返回数据乱码或为空 | 请求头或响应解码问题。 |
1. 确保
POST
请求的
Content-Type
是
application/x-www-form-urlencoded
,并且 payload 是
URLSearchParams
格式的字符串。
2. 网易云音乐可能对请求头有校验,如
Referer
,
User-Agent
,需要模拟浏览器设置。
|
5.2 高级调试技巧
-
“抠代码”与本地补环境
:如果加密函数依赖了浏览器特有的对象(如
window、document)或其他未定义的全局变量,直接复制到 Node.js 会报错。这时需要“补环境”,即在 Node.js 中模拟这些对象。更稳健的做法是只“抠出”最核心的加密函数(通常是纯数学计算),而用 Node.js 的crypto库替换掉原函数中识别出的 AES/RSA 部分。 -
使用
vm2或jsdom执行原 JS :对于极其复杂或混淆严重的代码,可以尝试将整个加密函数所在的JS文件或代码段,在 Node.js 中用vm2沙箱或jsdom模拟的浏览器环境中执行。这需要处理大量的依赖和环境变量,难度较大,但能保证 100% 还原。 -
关注代码更新
:网易云音乐的加密逻辑并非一成不变。如果某天你的脚本突然全部失效,首先应检查核心加密函数是否还在原位置,其内部的常量(
n,e,iv)和算法步骤是否有变化。重新抓包、断点、分析一遍是唯一的办法。
5.3 安全与伦理边界
最后必须强调,逆向工程技术是一把双刃剑。
-
学习与研究
:本文的目的是为了学习前端安全机制、加密算法应用和
JavaScript逆向技术,理解大型互联网应用如何保护其接口。 - 尊重版权与服务条款 :切勿将技术用于大规模爬取受版权保护的音乐内容、干扰网易云音乐正常服务、进行恶意攻击或商业牟利。这违反其服务条款,也可能涉及法律风险。
- 控制请求频率 :即使在个人学习测试时,也应控制请求频率,避免对对方服务器造成不必要的压力。
技术的乐趣在于探索和理解。通过这次对网易云音乐加密参数的逆向,我们不仅获得了一套可用的模拟请求方法,更重要的是,深入理解了前端加密的常见模式、非对称与对称加密的结合使用,以及如何系统性地进行
JS
逆向分析。这套方法论,完全可以迁移到分析其他具有类似前端加密策略的网站或应用上。当你再遇到
params
和
encSecKey
时,相信你已能从容应对。

2168

被折叠的 条评论
为什么被折叠?



