网易云音乐JS逆向

深藏阁楼爱情的钟 2022-10-26 01:27 916阅读 0赞

手把手教你网易云音乐JS逆向

1. 目标数据

  • 1.搜索音乐,获取音乐id
  • 2.获取url,下载音乐

2. 文章目录

目录

    • 手把手教你网易云音乐JS逆向
      1. 目标数据
      1. 文章目录
      1. 页面分析
      1. 模拟加密,获取参数
    • 5.获取url,下载音乐
      1. 歌曲搜索
      • 完美收工~~~
      • [下一篇 打造网易云图形界面](https://blog.csdn.net/zly717216/article/details/113548197)

3. 页面分析

用chrome浏览器进入网易云音乐官网,找到一首你喜欢的歌
在这里插入图片描述

进入歌曲页面后,url中有一个参数id,因此可以猜想:如果当前页面中能找到歌曲的url,只需要用id构造url,就能获取歌曲的链接,但事实上没有那么简单(后面会详细分析)
在这里插入图片描述

按F12 ,打开调试面板,刷新。发现,当前页面是一个html页面
在这里插入图片描述
搜索歌曲id,但是网页源代码中并没有(所以前面的猜想不成立,得另寻他法)在这里插入图片描述
既然这样,歌曲可能是动态加载的,需要到js中找,点击 XHR,从第一个到最后一个连接,发现并没有当前页面的id(1398663411)
在这里插入图片描述
既然是动态加载,再点击播放试试。功夫不负有心人,果然找到了
在这里插入图片描述
既然找到了,那就打开看看它的庐山站面目,看到url,并且是m4a后缀,是不是很惊喜
在这里插入图片描述
果然没错,就是它
在这里插入图片描述
既然这样我们就试试吧,结果发现,能拿到数据

  1. import requests
  2. url = 'https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token='
  3. headers = {
  4. 'origin': 'https://music.163.com',
  5. 'referer': 'https://music.163.com/',
  6. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'
  7. }
  8. data = {
  9. 'params': '+i3P/nsviot5Ms6bDX+wUmyKAFRm7u3J7pdECwtbeVV10k41za7DjYIXSgCbNngYOO7GH+ADMz/pULLM6bItfAGGfjatL9xirPAWnwzGbr9uTul31ITnC9es8lu01n5eyP8svVHDCecGH57SU0u+hA==',
  10. 'encSecKey': '21c157c5c208498782f0cecee8b518ee8726bba93e2dcb6e280d4d0e4c6ceea3107e5eaab6ad55ca53c713cd9893d5cf11491dae043bebccacc1c95680fff17ba34ccdfc3e3c4863eca69b671ef510a44e3f68d6fc182222d6e55b6d0fa320bf166364c82aa6a2adb641d60e7f480d5809f9c7e1963f884c9c5cf80cd81e5e08'
  11. }
  12. res = requests.post(url, headers=headers, data=data)
  13. print(res.json())

在这里插入图片描述
但是问题来了,params和encSeckey是啥玩意儿?这么长,一看就知道是加过密的,接下来要开启解密模式了
点击面板右上角的全局搜索,搜索encSeckey
在这里插入图片描述
有3个js文件,一个一个去找吧
在这里插入图片描述
先别着急找,像下面这样,结合起来,在第一个js文件中找到的可能性大一些,既然这样,那就开始找吧
在这里插入图片描述
小插曲:上面为什要搜encSeckey,而不搜params呢?因为用params命名的使用范围要比encSeckey广,所以用encSeckey搜好一些(经验)

点击第一个js,进去后点击格式化,搜索encSeckey,发现有3处,在这里插入图片描述
滑到第二处,为什么是第二处呢?很简单,因为params和encSeckey在一起嘛,不在这你说在哪?好了,到这里,另外的两个js可以忽略了。
在这里插入图片描述
接下来就要分析参数了,我们往上嫖,发现两个参数分别是由bWv7o的encText、encSecKey两个属性。

接下来才是真正的js逆向了,敲黑板了!!!

既然params和encKeckey是由bWv7o产生的,那么我们就要分析bWv7o是什么了。不难发现bWv7o是由window.asrsea(JSON.stringify(i1x), bsK8C(["流泪", "强"]), bsK8C(XR1x.md), bsK8C(["爱心", "女孩", "惊恐", "大笑"]));产生的,那么接下来就找找window.asrsea是啥玩意儿

搜索一下window.asrsea,window.asrsea=d
在这里插入图片描述
这里d就不能去搜索了,不现实,原因你懂的。这里的d肯定是和window.asrsea在同一作用域内,要不然咋赋值,稀里哗啦宝一大堆错,既然这样就往上翻吧(因为是赋值,所以不会在下面,没人这么干)

在同一作用域内的就这么点,就一个d函数

  1. !function() {
  2. function a(a) {
  3. var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
  4. for (d = 0; a > d; d += 1)
  5. e = Math.random() * b.length,
  6. e = Math.floor(e),
  7. c += b.charAt(e);
  8. return c
  9. }
  10. function b(a, b) {
  11. var c = CryptoJS.enc.Utf8.parse(b)
  12. , d = CryptoJS.enc.Utf8.parse("0102030405060708")
  13. , e = CryptoJS.enc.Utf8.parse(a)
  14. , f = CryptoJS.AES.encrypt(e, c, {
  15. iv: d,
  16. mode: CryptoJS.mode.CBC
  17. });
  18. return f.toString()
  19. }
  20. function c(a, b, c) {
  21. var d, e;
  22. return setMaxDigits(131),
  23. d = new RSAKeyPair(b,"",c),
  24. e = encryptedString(d, a)
  25. }
  26. function d(d, e, f, g) {
  27. var h = { }
  28. , i = a(16);
  29. return h.encText = b(d, g),
  30. h.encText = b(h.encText, i),
  31. h.encSecKey = c(i, e, f),
  32. h
  33. }
  34. function e(a, b, d,, e) {
  35. var f = { };
  36. return f.encText = c(a + e, b, d),
  37. f
  38. }
  39. window.asrsea = d,
  40. window.ecnonasr = e
  41. }();

接下来分析d函数是干嘛的,看来看去该函数不就返归一个h对象嘛,不就相当于window.asrsea=h嘛,而params就是encText ,encSecKey 就是encSecKey;encText 是b(h.encText, i)产生的,encSecKey是由c(i, e, f)产生的,接下来打上断点,开始调试吧
在这里插入图片描述
刷新,点击播放,发现有4个参数

在这里插入图片描述
在这里插入图片描述
再点一首歌,发现e,f,g三个参数是固定的
在这里插入图片描述
综上,d参数是变化的,而且变化的地方是id,分析的时候就当它是固定的,接着往后走

h={},i=a(16),那么a(16)是什么?点击去,一探究竟
在这里插入图片描述
在这里插入图片描述
经过调试和分析,a函数返回一个16位随机字符串(既然是随机的,那么就可以让它固定)
在这里插入图片描述
在这里插入图片描述
接着往后走在这里插入图片描述
由此可见b函数,是用来加密的,而且是encText经过了两次加密,跳进去,看看这个加密函数

原来是AES加密,熟悉AES的,肯定敲开心,不熟悉也没关系,很容易
在这里插入图片描述

  1. function b(a, b) {
  2. // urf-8编码
  3. var c = CryptoJS.enc.Utf8.parse(b)
  4. , d = CryptoJS.enc.Utf8.parse("0102030405060708") // 固定值
  5. , e = CryptoJS.enc.Utf8.parse(a)
  6. // AES加密,e加密文本,c秘钥,d偏移量,CBC模式
  7. , f = CryptoJS.AES.encrypt(e, c, {
  8. iv: d,
  9. mode: CryptoJS.mode.CBC
  10. });
  11. return f.toString()
  12. }

接下来分析encSecKey参数,跳入到c函数,长这样
在这里插入图片描述

  1. function c(a, b, c) { // a,b,c是固定值
  2. var d, e;
  3. return setMaxDigits(131), // 跳进去,调试... 创建长度为132的数组
  4. d = new RSAKeyPair(b,"",c), //
  5. e = encryptedString(d, a)
  6. }

由此可知,encSeckey是经过RSA加密的,但是这里的a,b,c三个参数是固定值,因此这个参数也可以用固定值

好了,到这里js逆向分析就结束了

4. 模拟加密,获取参数

  1. class Encrypt:
  2. def __init__(self, text):
  3. self.data = {
  4. 'encSecKey': '01ec48cb405730aa77f993a988cc1f5bc1938511d75f49eddc581f2fe2aaf18988853200564b2d4b1312cf6e0bb344425addce5a4c81b38b89a5973900946bd100b0f1865d22d2a8e5dd8be208eb5d6eb2f71309a165daeffe95355e1e44edd65bdf28088fe4f5e835a7d9f7569fc2530f9d17c00b51cfafbe421eb462247ea3'
  5. }
  6. self.text = text
  7. self.key = '0CoJUm6Qyw8W8jud'
  8. def get_form_data(self):
  9. """生成表单参数"""
  10. # 随机秘钥参数,可以用固定值
  11. i = "4JknCzx6uEXUwxpU"
  12. # 两次加密
  13. first_encrypt = self.AES_encpyt(self.text, self.key)
  14. self.data['params'] = self.AES_encpyt(first_encrypt, i)
  15. return self.data
  16. def AES_encpyt(self, text, key):
  17. """AES加密"""
  18. # AES加密明文必须为16的整数倍
  19. padding = 16 - len(text.encode()) % 16
  20. text += padding * chr(padding)
  21. aes = AES.new(key.encode(), AES.MODE_CBC, b'0102030405060708')
  22. enctext = aes.encrypt(text.encode())
  23. return b64encode(enctext).decode('utf-8')

5.获取url,下载音乐

  1. class NeteaseCloudMusic:
  2. def __init__(self, song):
  3. self.url = 'https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token='
  4. reqstr = ''' authority: music.163.com method: POST path: /weapi/song/enhance/player/url/v1?csrf_token= scheme: https accept: */* accept-encoding: gzip, deflate, br accept-language: zh-CN,zh;q=0.9 cache-control: no-cache content-length: 434 content-type: application/x-www-form-urlencoded cookie: __root_domain_v=.163.com; _qddaz=QD.28yaab.3jymc6.kf0ihnaf; _ntes_nnid=e7c5f90265b4d5a5bcb511efebf7a890,1600596980395; _ntes_nuid=e7c5f90265b4d5a5bcb511efebf7a890; _iuqxldmzr_=32; WM_TID=OlHvFOuIVclAQFQUAEJvJZyLuh3MwtGb; NMTID=00ODCot1Uq8CvcXIUIMmKBlPfRiyfoAAAF3NHwibw; WM_NI=%2BWiHzgkFWg%2BON3YYI0rQzlpsOW8x4BPGt%2FWRNpkD3r2Utv8U1gx6RZgvmmJQ0IpSBgdk1GvY9uIQW6BfIN7lVoHo8z1BIoa%2FdLUgKwpx6twUKJtgDlexKOu7LqWGuYApZzg%3D; WM_NIKE=9ca17ae2e6ffcda170e2e6eeb9c844a3b1aba3b24489eb8eb6d15b929a9baaaa5cace70087b64e8ab18299d02af0fea7c3b92ae989a7a9f96da99a9988aa458eed97bacc3cb28fb68df3798d89f899b74a9499bcd0d65a8eb0a5a5b27af28bbc97bb5ff3b9b8d7d152a5aaa38ec95bf497c0b4c16da8b5ffa8f553fbab87b2d63e82ba87afb66896b18890bb72f39e8790e425a8949b88ca7db4a8fa95f65f8996bc88c768a7a885b0f83d90af99a8f85383b0969be637e2a3; hb_MA-9F44-2FC2BD04228F_source=www.baidu.com; JSESSIONID-WYYY=bERBG86BVbD29X%5C35acjg8ndIoGYPEZvQ8fc0t7WUnMu3KTujvG1zqfSMIG%2By4%2FZRz9hC%2FwBN0Mf%2B%2B1RJBK2TeR96X7l%2BmS%2FHhuuqBwl7yxwe4jQ%5ChzFoFgKylb3ZdOnw6%2FqsqaUYUrJ12EVVy0m66JVlQez0T5ijmgZuOsk0KcMnUe4%3A1611553513123; WEVNSM=1.0.0; WNMCID=kctjbv.1611551714155.01.0 origin: https://music.163.com pragma: no-cache referer: https://music.163.com/ sec-fetch-dest: empty sec-fetch-mode: cors sec-fetch-site: same-origin user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 '''
  5. self.headers = HeaderPrettyDict().pretty(reqstr)
  6. self.text = '{"ids":"[' + str(song['song_id']) + ']","level":"standard","encodeType":"aac","csrf_token":""}'
  7. self.name = song['song_name']
  8. self.singer = song['singer']
  9. def music(self):
  10. """获取音乐的url"""
  11. data = Encrypt(self.text).get_form_data()
  12. res = requests.post(self.url, headers=self.headers, data=data)
  13. song_url = res.json()['data'][0]['url']
  14. self.save(self.download(song_url))
  15. def download(self, url):
  16. """下载音乐"""
  17. headers = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'}
  18. res = requests.get(url, headers=headers)
  19. return res.content
  20. def save(self, content):
  21. """保存音乐"""
  22. # 当前文件目录
  23. path = os.path.dirname(__file__)
  24. # 检查'data'目录是否存在,不存在则创建目录
  25. if not os.path.exists(path+'\\data'):
  26. os.mkdir(path+'\\data')
  27. # 音乐保存路径
  28. music_path = path+'\\data'+f'\\{self.name} {self.singer}.m4a'
  29. # 保存
  30. if not os.path.exists(music_path):
  31. with open(music_path, 'wb') as f:
  32. f.write(content)

6. 歌曲搜索

以上只能实现单曲下载,下面将实现歌曲搜索功能

先分析一下url的结构:url有连个参数,一个是s(歌曲名),一个是type(类型:单曲、视频、歌词等)
在这里插入图片描述
type类型总结如下:










































数字 类型
1 单曲
10 专辑
100 歌手
1014 视频
1006 歌词
1000 歌单
1009 声音主播
1002 用户

步骤和前面一样,先找到数据来源
在这里插入图片描述
接下来就要分析请求网址和参数了,乍一看,和前面一模一样啊
在这里插入图片描述

开心吧!!!但是事与愿违,虽然参数一样,但是用前面的生成参数却无法get到数据,别慌,方法还是一样,断点调试

经过调试之后,我们发现就是d参数不同(d={"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"冬眠","type":"1","offset":"0","total":"true","limit":"30","csrf_token":""}),因此只要构造d参数就能请求到数据了

在这里插入图片描述
直接上代码吧

  1. class SearchMusic:
  2. def __init__(self, text):
  3. self.url = 'https://music.163.com/weapi/cloudsearch/get/web?csrf_token='
  4. reqstr = ''' authority: music.163.com method: POST path: /weapi/song/enhance/player/url/v1?csrf_token= scheme: https accept: */* accept-encoding: gzip, deflate, br accept-language: zh-CN,zh;q=0.9 cache-control: no-cache content-length: 434 content-type: application/x-www-form-urlencoded cookie: __root_domain_v=.163.com; _qddaz=QD.28yaab.3jymc6.kf0ihnaf; _ntes_nnid=e7c5f90265b4d5a5bcb511efebf7a890,1600596980395; _ntes_nuid=e7c5f90265b4d5a5bcb511efebf7a890; _iuqxldmzr_=32; WM_TID=OlHvFOuIVclAQFQUAEJvJZyLuh3MwtGb; NMTID=00ODCot1Uq8CvcXIUIMmKBlPfRiyfoAAAF3NHwibw; WM_NI=%2BWiHzgkFWg%2BON3YYI0rQzlpsOW8x4BPGt%2FWRNpkD3r2Utv8U1gx6RZgvmmJQ0IpSBgdk1GvY9uIQW6BfIN7lVoHo8z1BIoa%2FdLUgKwpx6twUKJtgDlexKOu7LqWGuYApZzg%3D; WM_NIKE=9ca17ae2e6ffcda170e2e6eeb9c844a3b1aba3b24489eb8eb6d15b929a9baaaa5cace70087b64e8ab18299d02af0fea7c3b92ae989a7a9f96da99a9988aa458eed97bacc3cb28fb68df3798d89f899b74a9499bcd0d65a8eb0a5a5b27af28bbc97bb5ff3b9b8d7d152a5aaa38ec95bf497c0b4c16da8b5ffa8f553fbab87b2d63e82ba87afb66896b18890bb72f39e8790e425a8949b88ca7db4a8fa95f65f8996bc88c768a7a885b0f83d90af99a8f85383b0969be637e2a3; hb_MA-9F44-2FC2BD04228F_source=www.baidu.com; JSESSIONID-WYYY=bERBG86BVbD29X%5C35acjg8ndIoGYPEZvQ8fc0t7WUnMu3KTujvG1zqfSMIG%2By4%2FZRz9hC%2FwBN0Mf%2B%2B1RJBK2TeR96X7l%2BmS%2FHhuuqBwl7yxwe4jQ%5ChzFoFgKylb3ZdOnw6%2FqsqaUYUrJ12EVVy0m66JVlQez0T5ijmgZuOsk0KcMnUe4%3A1611553513123; WEVNSM=1.0.0; WNMCID=kctjbv.1611551714155.01.0 origin: https://music.163.com pragma: no-cache referer: https://music.163.com/ sec-fetch-dest: empty sec-fetch-mode: cors sec-fetch-site: same-origin user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 '''
  5. self.headers = HeaderPrettyDict().pretty(reqstr)
  6. self.text = text
  7. def search(self):
  8. """搜索音乐,返回音乐列表"""
  9. data = Encrypt(self.text).get_form_data()
  10. res = requests.post(self.url, headers=self.headers, data=data)
  11. songlist = []
  12. songs = res.json()['result']['songs']
  13. for song in songs:
  14. item = { }
  15. # id、歌名、歌手、封面
  16. item['song_id'] = song['id']
  17. item['song_name'] = song['name']
  18. item['singer'] = song['ar'][0]['name']
  19. # item['song_pic_url'] = song['al']['picUrl']
  20. songlist.append(item)
  21. return songlist

完美收工~~~

下一篇 打造网易云图形界面

发表评论

表情:
评论列表 (有 0 条评论,916人围观)

还没有评论,来说两句吧...

相关阅读