网易云音乐JS逆向

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

1. 目标数据

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

2. 文章目录

3. 页面分析

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

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

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

import requests

url = 'https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token='
headers = {
    'origin': 'https://music.163.com',
    'referer': 'https://music.163.com/',
    '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'
}
data = {
    'params': '+i3P/nsviot5Ms6bDX+wUmyKAFRm7u3J7pdECwtbeVV10k41za7DjYIXSgCbNngYOO7GH+ADMz/pULLM6bItfAGGfjatL9xirPAWnwzGbr9uTul31ITnC9es8lu01n5eyP8svVHDCecGH57SU0u+hA==',
    'encSecKey': '21c157c5c208498782f0cecee8b518ee8726bba93e2dcb6e280d4d0e4c6ceea3107e5eaab6ad55ca53c713cd9893d5cf11491dae043bebccacc1c95680fff17ba34ccdfc3e3c4863eca69b671ef510a44e3f68d6fc182222d6e55b6d0fa320bf166364c82aa6a2adb641d60e7f480d5809f9c7e1963f884c9c5cf80cd81e5e08'
}

res = requests.post(url, headers=headers, data=data)
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函数

!function() {
    function a(a) {
        var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
        for (d = 0; a > d; d += 1)
            e = Math.random() * b.length,
            e = Math.floor(e),
            c += b.charAt(e);
        return c
    }
    function b(a, b) {
        var c = CryptoJS.enc.Utf8.parse(b)
          , d = CryptoJS.enc.Utf8.parse("0102030405060708")
          , e = CryptoJS.enc.Utf8.parse(a)
          , f = CryptoJS.AES.encrypt(e, c, {
            iv: d,
            mode: CryptoJS.mode.CBC
        });
        return f.toString()
    }
    function c(a, b, c) {
        var d, e;
        return setMaxDigits(131),
        d = new RSAKeyPair(b,"",c),
        e = encryptedString(d, a)
    }
    function d(d, e, f, g) {
        var h = {}
          , i = a(16);
        return h.encText = b(d, g),
        h.encText = b(h.encText, i),
        h.encSecKey = c(i, e, f),
        h
    }
    function e(a, b, d,, e) {
        var f = {};
        return f.encText = c(a + e, b, d),
        f
    }
    window.asrsea = d,
    window.ecnonasr = e
}();

接下来分析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的,肯定敲开心,不熟悉也没关系,很容易
在这里插入图片描述

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

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

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

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

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

4. 模拟加密,获取参数

class Encrypt:

    def __init__(self, text):
        self.data = {
            'encSecKey': '01ec48cb405730aa77f993a988cc1f5bc1938511d75f49eddc581f2fe2aaf18988853200564b2d4b1312cf6e0bb344425addce5a4c81b38b89a5973900946bd100b0f1865d22d2a8e5dd8be208eb5d6eb2f71309a165daeffe95355e1e44edd65bdf28088fe4f5e835a7d9f7569fc2530f9d17c00b51cfafbe421eb462247ea3'
        }

        self.text = text
        self.key = '0CoJUm6Qyw8W8jud'

    def get_form_data(self):
        """生成表单参数"""

        # 随机秘钥参数,可以用固定值
        i = "4JknCzx6uEXUwxpU"

        # 两次加密
        first_encrypt = self.AES_encpyt(self.text, self.key)
        self.data['params'] = self.AES_encpyt(first_encrypt, i)

        return self.data

    def AES_encpyt(self, text, key):
        """AES加密"""

        # AES加密明文必须为16的整数倍
        padding = 16 - len(text.encode()) % 16
        text += padding * chr(padding)

        aes = AES.new(key.encode(), AES.MODE_CBC, b'0102030405060708')
        enctext = aes.encrypt(text.encode())

        return b64encode(enctext).decode('utf-8')

5.获取url,下载音乐

class NeteaseCloudMusic:

    def __init__(self, song):

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

        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
                    '''
        self.headers = HeaderPrettyDict().pretty(reqstr)

        self.text = '{"ids":"[' + str(song['song_id']) + ']","level":"standard","encodeType":"aac","csrf_token":""}'
        self.name = song['song_name']
        self.singer = song['singer']

    def music(self):
        """获取音乐的url"""

        data = Encrypt(self.text).get_form_data()
        res = requests.post(self.url, headers=self.headers, data=data)

        song_url = res.json()['data'][0]['url']
        self.save(self.download(song_url))

    def download(self, url):
        """下载音乐"""

        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'}
        res = requests.get(url, headers=headers)
        return res.content

    def save(self, content):
        """保存音乐"""

        # 当前文件目录
        path = os.path.dirname(__file__)
        # 检查'data'目录是否存在,不存在则创建目录
        if not os.path.exists(path+'\\data'):
            os.mkdir(path+'\\data')
        # 音乐保存路径
        music_path = path+'\\data'+f'\\{self.name} {self.singer}.m4a'
        # 保存
        if not os.path.exists(music_path):

            with open(music_path, 'wb') as f:
                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参数就能请求到数据了

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

class SearchMusic:

    def __init__(self, text):

        self.url = 'https://music.163.com/weapi/cloudsearch/get/web?csrf_token='

        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
            '''
        self.headers = HeaderPrettyDict().pretty(reqstr)

        self.text = text

    def search(self):
        """搜索音乐,返回音乐列表"""

        data = Encrypt(self.text).get_form_data()
        res = requests.post(self.url, headers=self.headers, data=data)

        songlist = []
        songs = res.json()['result']['songs']

        for song in songs:
            item = {}

            # id、歌名、歌手、封面
            item['song_id'] = song['id']
            item['song_name'] = song['name']
            item['singer'] = song['ar'][0]['name']
            # item['song_pic_url'] = song['al']['picUrl']

            songlist.append(item)

        return songlist

完美收工~~~

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

  • 19
    点赞
  • 89
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值