0. 内容概述
获取一个用户的所有歌单(包括创建的歌单和收藏的歌单),将歌单的信息(包括封面/名称/歌曲文件)下载到本地。
- 可以选择下载一个用户的所有歌单所有歌曲
- 也可以下载一个歌单内的所有歌曲
难点:解析网易云音乐获取歌单和音乐mp3文件时发送的POST请求,模拟其加密方式,发送自己想要的请求。
工具:
- Firefox浏览器
- Python 3.8.2
- requests https://pypi.org/project/requests/
- BeautifulSoup4 https://pypi.org/project/beautifulsoup4/
- PyCrypto https://pypi.org/project/pycrypto/
- …
1. 网络请求分析
1.1 找到请求地址
登录网易云音乐网页版之后,进入“我的音乐”,打开开发者工具转到网络,清除掉所有消息后,点击一个自建的歌单,可以看到出现了几个请求,如下图所示。
因为GET请求只获取到了两个图片信息,因此推断歌曲信息包含在了POST请求中。
在新标签页打开其中后缀为detail的POST请求:
https://music.163.com/weapi/v6/playlist/detail?csrf_token=e7cd35dfd29adb489a4d5b8c3b3ef8fa
发现歌单信息包含在了该请求的返回结果中,包括歌曲的ID,专辑信息等
因此只需要模拟这个请求,就可以获取到歌单的内容。
1.2 POST请求参数分析
1.2.1 请求内容简略
在浏览器中查看POST请求参数,发现两道密文:
将发送该请求的js文件下载下来,很明显是经过webpack打包过的,搜索encSecKey
,找到如下代码:
将包含这两个参数的完整函数提取出来,得到如下格式化结果:
1 | encryptFetch = function (e) { |
这段代码用到了大量的匿名变量,但是很显然,加密params
和encSecKey
参数的代码由encrypt.asrsea
函数完成,因此接下来解析该函数的输入参数和构成,按照数据流的方式进行。
1.2.2 加密函数和加密算法分析
第一个参数:JSON.stringify(r)
函数encryptFetch
意思是为请求加密,那么推测,其唯一参数e
为网络请求的地址,在函数的第一行写到:o=e, i=o.split("?")
,猜测是对请求地址中的参数进行提取;
在14行处,2===i.length && (r = query2obj(i[1]))
很显然了,如果该请求存在参数,那么用?
分离出来正是位于第二位,使用query2obj
将其转换成一个JS对象;
在20行,在cookie
中获取了一个跨域保护参数__csrf
,并将其加入到JS对象r
中;
在21行,对网络请求的地址进行了修改,在__csrf
不为空的情况下,将其作为参数写入到请求地址o
中去,这个o
在加密时并没有用到
综上所述,JSON.stringify(r)
是请求地址参数的字符串化结果,一个例子如下:
1 | // encrypyFetch('https://aaa.com/?mode=123') |
第二/第三/第四个参数均来自enk
,在JS文件中搜索,找到这个变量的所有相关代码:
1 | var encrypt = { asrsea: d$1, ecnonasr: e$1 }, |
参数在整个js文件中搜索asrsea
,并将其相关的函数提取出来,得到如下代码:
1 | var encrypt = { asrsea: d$1, ecnonasr: e$1 } |
加密函数的入口是d$1
函数,在该函数中,encText
使用b
函数进行了两层加密,encSecKey
使用c$1
函数进行了一层加密,两个参数之间的关联在于由a$1
函数生成a
变量,该变量就是一个由数字和字母构成的16位长度的随机字符串。
对encText
的加密过程为:使用enk.emj2code(["爱心", "女孩", "惊恐", "大笑"])
生成的字符串0CoJUm6Qyw8W8jud
作为密钥(KEY)、0102030405060708
作为偏移量(IV)进行加密,接着再用随机字符串a
作为密钥,同样的偏移量进行二次加密;
aes的加密由crypto-js实现
对encSecKey
的加密用到了RSA方法,其加密方法如下:
其中,是信息,是次方,对应代码中的"010001"
的10进制数65537,是模大小,对应代码的enk.BASE_CODE
,字符串长度258位,转为10进制为309位,是加密后的密文。
总结一下加密过程:首先生成一个16位的随机字符串a
与常量字符串0CoJUm6Qyw8W8jud
一起,作为加密和解密POST参数的密钥,用到了对称的的AES算法,密文放在params
参数中;参数encSecKey
存放的则是通过RSA算法加密后的字符串a
。
1.2.3 POST请求内容分析
要分析请求内容,一般是通过在浏览器相关代码处打断点,查看变量实现的,或者极端一点,直接破解RSA算法加密的密文,反解出参数内容,然后破解成功第二天直接到网易报道上班。
获取歌单内容的请求链接为
https://music.163.com/weapi/v6/playlist/detail?csrf_token=e7cd35dfd29adb489a4d5b8c3b3ef8fa
发起者是名为musicfrontencryptvalidator.min.js
的文件,在这个文件中找到encSecKey
,并打上断点,再点击另一个歌单,发现歌单刷新了却还是无法命中断点,一开始还以为是浏览器不支持,骂了一通Firefox并装了个Chrome,后来发现Chrome也做不到还强行装在了C盘,又骂骂咧咧地把Chrome卸了重新回来找问题。
终于在刷新了一遍又一遍网页之后,注意到一个获取playlist
的POST请求:
https://music.163.com/weapi/user/playlist?csrf_token=e7cd35dfd29adb489a4d5b8c3b3ef8fa
发起者是名为core_68ac1b3aadf40a20caba599a0ab2365d.js
的文件,其请求参数也是params
和encSecKey
!
在这个文件中找到encSecKey
打上断点,切换歌单,终于看到了希望看到的参数:
参数为:
1 | { |
其中的id是歌单的id,那么好了,只需要获取到歌单的ID,就可以获取到其内容了。
2. Python实现参数加密和数据爬取
代码参考自https://github.com/Jack-Cherish/python-spider,用到了PyCrypto库作为AES的加密工具,使用pip安装时提示某个.h文件中存在语法错误,推测是C++的包出错导致的,参考了一个解决方案:https://blog.csdn.net/airconan/article/details/88386378,打开VS2019的安装目录,找到C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.27.29110\include\stdint.h
,将其拷贝至C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\ucrt\stdint.h
的同一目录下,并将这个文件中的#include <stdint.h>
改为#include "stdint.h"
,重新pip安装,即可安装成功。
1 | import requests |