网易云音乐api解析

网易云音乐api解析


前几天去网易云听歌的时候发现一首挺喜欢的冷门音乐下载要钱,对于我这种平时一分钱都没有的学生党来说购买vip自然是奢侈的事情,于是打算自己写一个爬虫来获取网易云音乐的下载地址,忙活两天弄懂以后决定把自己研究的过程放出来

准备:

* 一个能用控制台的浏览器
* Fiddler4,可以去fiddler官网下载
* Java/C#/js基础知识
* Postman

获取请求

  网易这种大公司自然不会把页面写成静态的,所以第一件事情是点击播放之后按F12,过滤出所有XHR请求并且挨个查看响应,很明显图片里面的请求就是获取歌曲源的请求.

图片

接下来查看该请求的请求头可以发现请求的链接是

https://music.163.com/weapi/song/enhance/player/url?csrf_token=
图片
  请求方法是post,接着往下看可以看到post的具体数据,一个params一个encSecKey,这种让人头疼的乱码字符串一看就知道是经过加密的,那么,该从哪里分析这一串加密数据呢.首先要明白一点,不管密文有多么复杂,加密的方法一定是在网页的源代码里面实现的,而且post的时候一定会出现paramsencSecKey两串字符,可以从这里下手,而网页的源代码通过F12->source可以轻易的查看到(这是chrome的方法,别的浏览器大同小异),通过网页的源码堆里面挨个搜索这两个关键字
图片
  发现这个core_开头加上一串字符的JavaScript文件里面包含了三个encSecKey,刚打开这个js文件的时候浏览器会巨卡无比,而且很大几率搜索匹配文本的时候浏览器会直接挂掉,解决方法是点一下pretty print格式化文本,就是左下角那一对花括号
图片
嗯,赏心悦目
  解决文本可视化的问题后接下来就是分析params和encSecKey这两个参数是怎么取得的了,搜索params有39处,因为params这个单词在很多函数里也有用到,所以选择搜索匹配encSecKey




  如图,一共有三处,分别是12865行一处,12985行两处
顺便从第一处encSecKey向上翻会发现这么一堆傻子都知道是加密函数的东西

图片
  至于这几个加密函数有什么用先不管,先来顺藤摸瓜找params和encSecKey
图片
  可以看出params和encSecKey分别是变量bVs9j的两个属性,encText很明显就是params,bVs9j则是经过方法asrsea的输出,参数分别是:
1
2
3
4
JSON.stringify(i4m)
brK9B(["流泪", "强"])
brK9B(Xw3x.md)
brK9B(["爱心", "女孩", "惊恐", "大笑"])


  第一个是一个json字符串,大概可以猜得出来是要post的数据,而后面三个是什么鬼东西我反正是不知道,不知道怎么办,打断点
图片
  当然这个时候你用fiddler的AutoResponder本地js替换线上加几个console.log也是可以的,不过目前很明显打断点要方便得多,打完断点之后刷新页面逐行执行代码
图片
  这个时候各个值差不多就已经一清二楚了,当然肯定不能把这些值直接丢到参数里面,把鼠标移到asrsea()上面
图片
出来一个链接到12860行的东西,点一下
图片
Bingo!很明显d函数就是asrsea(),传进去的对应四个参数分别是
1
2
3
4
d = JSON.stringify(i4m)
e = brK9B(["流泪", "强"])
f = brK9B(Xw3x.md)
g = brK9B(["爱心", "女孩", "惊恐", "大笑"])


  可以看到这些参数分别被传进a,b,c这些方法里面,而d实际上return了h,h的encText经过b函数处理了两次,第一次是b(d, g)第二次是b(h.encText, i),而i是a(16)的值,看来问题的中心就在于a,b,c三个方法里面,接着往上翻
图片
  这三个函数的作用不言而喻,但是还是来分析一下
  先来分析a
  第一行不用管,第二行for循环了a次,每一次都给e赋一个新值,操作是取不大于0-1之间的随机数与b的长度之积的最大整数,然后将字符串b的第e个添加的c的末尾,很明显e是一个类似坐标的变量,每循环一次都从b中取一个随机的字符并且赋给c,循环完成后返回c,因为在d方法里面a的参数是一个常数16,所以可以得知a方法的作用是获取一个随机16位字符串,b和c大概不需要我说,一个是AES算法加密,一个是RSA算法加密,要提两点的是b方法中aes加密的向量(iv)是0102030405060708,在方法里面已经标了出来,c方法和普通的rsa加密大概有一些区别,往上面翻到setMaxDights方法
图片
我js不是很好不是很明白这个方法是干嘛的= =,看上去是一个补0,暂时不管,接下来看encryptedString
图片
……头疼..
  秉着这种位运算加密函数一概不看的想法我决定先往下面走,回到d方法,也就是asrsea上面,虽然分析了传进去的参数都是干嘛用的,但是似乎这些参数的值还不知道,控制台log一下
图片
  实际后续我又重试输出了几次,接着换了一首歌又输出了几次,结果完全一样,说明这三个参数实际上是定值,接下来获取JSON.stringify(i4m)得值,断点每一步输出一次
图片
  这么多值肯定不是每个都是在请求音乐资源,加载一首歌曲的页面要发送很多请求,比如评论,歌词,封面,歌曲本身,因为在网络请求里面看到的xhr绝大部分post参数都是params和encSecKey,而js里面加密函数只有d这么一个,猜测所有请求的加密都是走的这个函数,因此会多次输出结果不一致,辨别方法很简单,还记得在网页请求里面的歌曲资源请求api吗:

https://music.163.com/weapi/song/enhance/player/url?csrf_token=

断点时候截图可以看到Y4c就是实际请求api的链接
图片
接下来断点每执行一次就看一下,最终发现与请求api对应的格式是
图片
1
{"ids":"[32648305]","br":128000,"csrf_token":""}


  不用说32648305对应的就是歌曲id,而br和csrf_token多次输出证明是常量,然后解决方法就一目了然了,所以我们只需要用C#/java重写一下加密函数就好了吗?nonono,加密函数一共有三个,而且RSA加密的函数看着让人头疼,于是我决定再看一看能不能避开这个函数,上面提到了
图片
  这三个都是定值,而且d的后三个参数正是这些。d方法里面第一次加密encText的时候用到了b(d, g),d是待加密的字符串,g是aes加密的key,也就是常量0CoJUm6Qyw8W8jud
图片
  而encSecKey的生成则是借助于a(16),也就是说,只要a(16)生成的16位字符串key不变,encSecKey就是定值,那么可不可以把encSecKey当做常量对待呢?用postman(使用方法自行百度)发现确实更改params但是不更改encSecKey不影响结果,也能正常获取到响应数据
图片
  到了这一步,请求的所有要素都已经拼接完善了,过程大略就是

拼接json字符串->使用0CoJUm6Qyw8W8jud作为key加密一次->使用随机16位随机字符串作为key再加密一次得到params参数->将第二次的16位随机字符串rsa加密一次得到encSecKey参数,因为encSecKey参数仅与16位随机字符串有关,与其他变量无关,因此可以当做常量对待->拼接字符串->发送post请求->获取歌曲资源

最后一个问题是既然encSecKey是常量那么在请求参数里面带上的意义是什么呢,因为我们对json数据的第二次加密是用的随机生成的16位key,因此在post的时候需要附带上这个key的信息在后台解密用,所以encSecKey是必不可或缺的参数

代码:

Encrypter解密类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
[SuppressMessage("ReSharper", "IdentifierTypo")]
public static class Encrypter
{
//AES向量
private const string Iv = "0102030405060708";
public const string EncryptKey = "0CoJUm6Qyw8W8jud";
public const string EncryptKey2 = "a8LWv2uAtXjzSfkQ";

public const string EncSecKey =
"2d48fd9fb8e58bc9c1f14a7bda1b8e49a3520a67a2300a1f73766caee29f2411c5350bceb15ed196ca963d6a6d0b61f3734f0a0f4a172ad853f16dd06018bc5ca8fb640eaa8decd1cd41f66e166cea7a3023bd63960e656ec97751cfc7ce08d943928e9db9b35400ff3d138bda1ab511a06fbee75585191cabe0e6e63f7350d6";

public static string GeneratePostData(string data)
{
return data.EncryptToAes(EncryptKey).EncryptToAes(EncryptKey2).EncodeTo();
}

public static string EncryptToAes(this string encryptText, string key)
{
if (string.IsNullOrEmpty(encryptText))
{
throw new ArgumentOutOfRangeException(nameof(encryptText), @"text cannot be null");
}

try
{
var bytesFromEncryptText = encryptText.GetBytes(Encoding.UTF8);

var aesEncrypter = new RijndaelManaged
{
Key = key.GetBytes(Encoding.UTF8),
IV = Iv.GetBytes(Encoding.UTF8),
Mode = CipherMode.CBC
};

var cryptoTranformer = aesEncrypter.CreateEncryptor();
var resultBytes =
cryptoTranformer.TransformFinalBlock(bytesFromEncryptText, 0, bytesFromEncryptText.Length);
return resultBytes.ToBase64String(0, resultBytes.Length);
}
catch (Exception)
{
return null;
}
}
}

注:EncodeTo()是自己写的扩展方法,用处是将字符串转换为URL编码,这一步很重要,如果不这么做是得不到响应的

拼接post参数

1
2
public static Func<string, string> GetWork = workId =>
$"{{\"ids\":\"[{workId}]\",\"br\":128000,\"csrf_token\":\"\"}}";


拼接完整的请求参数

1
var postData = $"params={Encrypter.GeneratePostData(NetEaseApiInterfaceRules.GetWorksList(keyWord, pageOffset))}&encSecKey={Encrypter.EncSecKey.EncodeTo()}"


发送请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public byte[] SendRequest(string postData = null)
{

var client = new WebClient();
if (_header != null)
{
foreach (var keyValuePair in _header)
{
client.Headers.Add(keyValuePair.Key, keyValuePair.Value);
}
}

return postData != null
? client.UploadData(_url, postData.GetBytes(Encoding.UTF8))
: client.DownloadData(_url);
}


解析请求类的完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[SuppressMessage("ReSharper", "StringLiteralTypo")]
public class GetSongWorksCrawler
{
private static readonly Dictionary<string, string> Header = new Dictionary<string, string>
{
{"User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36" },
{"Referer", "http://music.163.com" },
{"Content-Type", "application/x-www-form-urlencoded" }
};
public static WorksListDeserializer.Result GetSongListPerPage(string keyWord, int pageOffset)
{
var postData =
$"params={Encrypter.GeneratePostData(NetEaseApiInterfaceRules.GetWorksList(keyWord, pageOffset))}&encSecKey={Encrypter.EncSecKey.EncodeTo()}";

var worksJsonResponse = new WebUtilities(NetEaseApiInterfaceRules.GetWorksListUrl, Header).SendRequest(postData)
.GetString(Encoding.UTF8)
.DeserializeJsonText<WorksListDeserializer.SearchWorksList>()
.result;
return worksJsonResponse;
}

public static SongWork.Datum[] GetSongWork(string id)
{
var postData =
$"params={Encrypter.GeneratePostData(NetEaseApiInterfaceRules.GetWork(id))}&encSecKey={Encrypter.EncSecKey.EncodeTo()}";

var workJsonResponse = new WebUtilities(NetEaseApiInterfaceRules.GetWorkUrl, Header).SendRequest(postData)
.GetString(Encoding.UTF8)
.DeserializeJsonText<SongWork.WorkDetail>()
.data;
return workJsonResponse;
}
}
0%