JS逆向之网易云音乐&Python爬虫之网易云音乐爬取


前言

缺点:不能够爬会员歌曲

网易云音乐网页的源代码里没有下载歌曲的url,开发者工具里也无法在浏览器渲染后的页面代码里找到,所以–进行抓包。
在这里插入图片描述
抓包发现目标。
访问一下url
在这里插入图片描述


一、分析请求

抓到的请求为Post请求,有两个加密的参数params和encSecKey。
示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。
在这里插入图片描述

二、探索加密的方法

1.分析调用栈

在这里插入图片描述

进入(anonymous),打上断点运行几次后,发现了目标url,但是参数params和encSecKey已经加密了,这个文件里也找不到加密逻辑,推测是在之前的js文件中加密的。(其他的也是同理)
且需注意在这些js文件中加密后的参数是以这种形式存在变量中:

"params=****(省略)&encSecKey=****(省略)"

注意:
在这些js文件中params和encSecKey的值都是经过urlencode编码的。
urlencode编码可以保证URL中不含任何非法字符,让请求可以正确传递参数。
例如,空格会被转换为"%20",
加号会被转换为"%2B"等。
当浏览器发送URL请求时,浏览器会自动对参数值进行URL编码,因此我们不需要手动进行编码。(所以可以直接用未进行urlencode编码的dict发送请求),而且在服务器端接收到请求时,也会自动进行解码。

import urllib.parse
from urllib.parse import urljoin, urlencode, quote, unquote
模拟浏览器编码
string2 = 'ox%2BZw69bRVp1Zno8yEmCWWDQnlUZDKdQkMQ9%2B0Qle1FQJVWCRKWeNiGPRth9GuDDXTp%2FwfRCrIpYU2FoCVppjzKKmuqMTx98jSFM4Z%2BAR2i1FqLCCQnnaH2LxmSXcu4CnpnTWxEb8bepr5rjwQaQzx4TH1%2FmeEd%2FQEjxFt9VTgmeLPAk89mYlcoflouTbakVejhG1JoJuvYUKBUG1i3SLwZ0%2Br%2FArh0%2FcFR9gUxJA0Xa7zhu%2BlThKbj5cIMR1%2BNum2gXwBpY2ZaBJp2ItIbpaDyhoXfCo42gdV1j0PNExo7xZlkKLaazkVgz6Z%2FTM7AyRm6F26sx7P5HYlFC8yyJILYcNecVqhUKpzN143O6U8h6Fmn4Y43EHF5ADBRXhdtWNH0gGJ7KMtxF%2Fw%2Fl2ugBOiaclcRRsXTrxowJL%2FMxXJgTvz97g%2BmzXuCmj4VTdq5PUccUoJ896c%2FK3xy2CJF36TqsgE7yAMeKs2KRu6Ki%2BZh1W8pqjhH9yejv%2B7O7xtMz8Yoo8r8laJUF1L1GJwSRtUCie%2FdFhgWPYl2D7A6kc6YNOifjnxV7A73KT6GGdAr1rI9bDFNKNy2pdSBd1hRc6hvuV%2BbLXuVYI5NHvIicHTboAfZk5F4BS%2FPgaWQ5e6raefStTKoggIkTIdhncKNq%2Bw%3D%3D'
decoded_param = urllib.parse.unquote(string2)
print(decoded_param)
ox+Zw69bRVp1Zno8yEmCWWDQnlUZDKdQkMQ9+0Qle1FQJVWCRKWeNiGPRth9GuDDXTp/wfRCrIpYU2FoCVppjzKKmuqMTx98jSFM4Z+AR2i1FqLCCQnnaH2LxmSXcu4CnpnTWxEb8bepr5rjwQaQzx4TH1/meEd/QEjxFt9VTgmeLPAk89mYlcoflouTbakVejhG1JoJuvYUKBUG1i3SLwZ0+r/Arh0/cFR9gUxJA0Xa7zhu+lThKbj5cIMR1+Num2gXwBpY2ZaBJp2ItIbpaDyhoXfCo42gdV1j0PNExo7xZlkKLaazkVgz6Z/TM7AyRm6F26sx7P5HYlFC8yyJILYcNecVqhUKpzN143O6U8h6Fmn4Y43EHF5ADBRXhdtWNH0gGJ7KMtxF/w/l2ugBOiaclcRRsXTrxowJL/MxXJgTvz97g+mzXuCmj4VTdq5PUccUoJ896c/K3xy2CJF36TqsgE7yAMeKs2KRu6Ki+Zh1W8pqjhH9yejv+7O7xtMz8Yoo8r8laJUF1L1GJwSRtUCie/dFhgWPYl2D7A6kc6YNOifjnxV7A73KT6GGdAr1rI9bDFNKNy2pdSBd1hRc6hvuV+bLXuVYI5NHvIicHTboAfZk5F4BS/PgaWQ5e6raefStTKoggIkTIdhncKNq+w==

模拟服务器解码
dict={"string1":'ox+Zw69bRVp1Zno8yEmCWWDQnlUZDKdQkMQ9+0Qle1FQJVWCRKWeNiGPRth9GuDDXTp/wfRCrIpYU2FoCVppjzKKmuqMTx98jSFM4Z+AR2i1FqLCCQnnaH2LxmSXcu4CnpnTWxEb8bepr5rjwQaQzx4TH1/meEd/QEjxFt9VTgmeLPAk89mYlcoflouTbakVejhG1JoJuvYUKBUG1i3SLwZ0+r/Arh0/cFR9gUxJA0Xa7zhu+lThKbj5cIMR1+Num2gXwBpY2ZaBJp2ItIbpaDyhoXfCo42gdV1j0PNExo7xZlkKLaazkVgz6Z/TM7AyRm6F26sx7P5HYlFC8yyJILYcNecVqhUKpzN143O6U8h6Fmn4Y43EHF5ADBRXhdtWNH0gGJ7KMtxF/w/l2ugBOiaclcRRsXTrxowJL/MxXJgTvz97g+mzXuCmj4VTdq5PUccUoJ896c/K3xy2CJF36TqsgE7yAMeKs2KRu6Ki+Zh1W8pqjhH9yejv+7O7xtMz8Yoo8r8laJUF1L1GJwSRtUCie/dFhgWPYl2D7A6kc6YNOifjnxV7A73KT6GGdAr1rI9bDFNKNy2pdSBd1hRc6hvuV+bLXuVYI5NHvIicHTboAfZk5F4BS/PgaWQ5e6raefStTKoggIkTIdhncKNq+w=='}
encode_param = urlencode(dict)
print(encode_param)
string1=ox%2BZw69bRVp1Zno8yEmCWWDQnlUZDKdQkMQ9%2B0Qle1FQJVWCRKWeNiGPRth9GuDDXTp%2FwfRCrIpYU2FoCVppjzKKmuqMTx98jSFM4Z%2BAR2i1FqLCCQnnaH2LxmSXcu4CnpnTWxEb8bepr5rjwQaQzx4TH1%2FmeEd%2FQEjxFt9VTgmeLPAk89mYlcoflouTbakVejhG1JoJuvYUKBUG1i3SLwZ0%2Br%2FArh0%2FcFR9gUxJA0Xa7zhu%2BlThKbj5cIMR1%2BNum2gXwBpY2ZaBJp2ItIbpaDyhoXfCo42gdV1j0PNExo7xZlkKLaazkVgz6Z%2FTM7AyRm6F26sx7P5HYlFC8yyJILYcNecVqhUKpzN143O6U8h6Fmn4Y43EHF5ADBRXhdtWNH0gGJ7KMtxF%2Fw%2Fl2ugBOiaclcRRsXTrxowJL%2FMxXJgTvz97g%2BmzXuCmj4VTdq5PUccUoJ896c%2FK3xy2CJF36TqsgE7yAMeKs2KRu6Ki%2BZh1W8pqjhH9yejv%2B7O7xtMz8Yoo8r8laJUF1L1GJwSRtUCie%2FdFhgWPYl2D7A6kc6YNOifjnxV7A73KT6GGdAr1rI9bDFNKNy2pdSBd1hRc6hvuV%2BbLXuVYI5NHvIicHTboAfZk5F4BS%2FPgaWQ5e6raefStTKoggIkTIdhncKNq%2Bw%3D%3D

一直探索直到进入了u4y.be5j:
在这里插入图片描述
打上断点运行几次后发现目标Url(X5c)和加密后的params和encSecKey(e4i)但是虽然参数也是密文(这里的密文是未经过urlencode编码的,可以直接拿来给Python发请求),但是这个文件中有加密逻辑:
在这里插入图片描述
发现猫腻,params和encSecKey在这里被给值了,而且是通过asrsea这个函数生成的bVgix给定的值。
断点打在这,观察到i4m的值有考究,其他的三个参数都是固定值:
在这里插入图片描述
参数中ids在当前网页的url中可以找到:

https://music.163.com/#/song?id=2084135171

level和encodeType在多个歌曲播放页的值都相同,是固定给的值.
csrf_token是空,这个根据请求参数也可以确定没问题

{
    "ids": "[2084135171]",
    "level": "standard",
    "encodeType": "aac",
    "csrf_token": ""
}

那么进入到window.asrsea当中看看我们要的两个请求参数是怎么生成的:

!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
    } 
}();

看懂代码逻辑:

    #e,f,g都是固定值,d的值为:
   # {
   #"ids": "[2084135171]",
   #"level": "standard",
   # "encodeType": "aac",
   # "csrf_token": ""
   # }
    function d(d, e, f, g) {
        var h = {}
        var i = a(16);  随机生成一段字符串 ,因为这里是随机生成的,所以可以替换成固定的随机生成值来使用
        //用b函数对d加密了两次
        //iv= CryptoJS.enc.Utf8.parse("0102030405060708") 
        h.encText = b(d, g)  //iv=固定值 密钥为g
        h.encText = b(h.encText, i) //iv= 固定值 密钥为i (注意I为随机生成的值),而服务器要想解密必需得有i
        //加密两次后encText的值就确定好了
        h.encSecKey = c(i, e, f) // i通过AES加密发送给服务器进行解密获得i然后解密encText
        return h
    }
   //b函数就是很直接的AES加密
    function b(a, b) {
        var c = CryptoJS.enc.Utf8.parse(b) //将给定的字符串 b 解析为 UTF-8 编码的字节
          , 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() //返回字符串
    }
//c函数是RSA加密 使用用公钥加密 服务器用私钥解密
//注意这里的RSA加密是网易自己实现的,而且稍微有点改动。
function c(a, b, c) {
    var d, e;
    setMaxDigits(131);//无意义
    d = new RSAKeyPair(b,"",c);  //先搞定公钥 b,c的值是固定的
    e= encryptedString(d, a);    //对a进行加密
    return e
}

2.实现加密

    //方案1
    //因为i为随机生成的密钥,所以我们可以给i设定为一个固定的可得到随机值
    //那么h.encSecKey的值就可以定死(i加密后的值)
    // i="xS5OktXaZxEDoVUb"  //固定一个随机值
    // h.encSecKey="aa60724b374a002869b49ae267bef6d278f3f8d736da359f96435f7d955ccd22e2d6898c5acfa40d457afb232469ffa42ceba193e54f297eba196442a15bbd537b73ec8dae371c06c2699a1ca71ac48bee000473a38eb3b07ab77d1f952e5f7d94762868eb08145442d01ed68960bcabbe9ec3109ac232b3f41f9a01a49ef48f"
   

这个固定的随机值可以从浏览器的控制台获取:
在这里插入图片描述

方案二:
就是我们自己也实现以下RSA加密,加密逻辑和网易的一样:

生成RSA密钥对的过程通常是以下步骤:

随机选择两个大质数 p 和 q。

计算 n = p*q。

计算欧拉函数 φ(n) = (p-1)*(q-1)。

随机选择一个整数 e,使得 1 < e < φ(n),且 e 与 φ(n) 互素。e 成为公钥的一部分。

使用扩展欧几里得算法计算整数 d,使得 (d*e) mod φ(n) = 1。d 成为私钥的一部分。

最后,公钥由 (n, e) 组成,私钥由 (n, d) 组成。

了解一下网易RSA加密怎么加密:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

这个BarrettMu_powMod是上面的powMod函数:
biShiftRight是对字符串进行翻转(网易RSA加密的特别之处)
在这里插入图片描述

#对I加密
def jm2(i, e, f):
   # e,f是用于RSA加密的参数(都是字符串) 明文(数字) ** e mod f => 密文(数字)  加密用的都是数字
    e=int(e,16) #字符串转为16进制数字
    f=int(f,16) #字符串转为16进制数字
    i=i[::-1]  #网易多搞的一步字符串反转 star:stop:step  stop默认0 stop默认-1 step为-1就能让字符串反转
    bs=i.encode("utf-8") #把字符串变成字节
    s=binascii.b2a_hex(bs).decode() #把字节转为16进
    #制的数字后解码为字符串得到就是16进制数字代表的字符串
    s=int(s,16) #16进制数字代表的字符串转为整数
    mi=(s**e)%f #
    print(format(mi,"x")) #转为16进制的数字就是密文
    return format(mi,"x")

然后用Python实现参数加密:

import base64
import binascii
import json
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad


如果网站对自己的数据进行保护就可能会对数据进行加密:
“One”=> 加密为一堆无法辨认的字节
http协议对于字符串的发送和处理效率高,好处理
对于字节的发送和传输效率就较低,且处理起来比较麻烦。


解决办法:
把加密产生的字节信息处理成http容易处理的字符串的方法---base64
base64-->组成A~Z,a~z,0~9,/,+64个符号)
base64可以把字节变成base64形式的字符串,也可以把字符串还原为字节
base64处理字节时33个一起处理,处理成4个字符的字符串

def jm1(data,key,iv):
    key=key.encode('utf-8')
    data1=data.encode('utf-8')
    data1=pad(data1, 16)
    aes = AES.new(key=key, mode=AES.MODE_CBC, IV=iv)
    res = aes.encrypt(data1) //得到字节
    res=base64.b64encode(res).decode() //编码未base64形式的字节后解码为字符串
    return res

# 方案1对I加密
# def jm2(i, e, f):
#     return "aa60724b374a002869b49ae267bef6d278f3f8d736da359f96435f7d955ccd22e2d6898c5acfa40d457afb232469ffa42ceba193e54f297eba196442a15bbd537b73ec8dae371c06c2699a1ca71ac48bee000473a38eb3b07ab77d1f952e5f7d94762868eb08145442d01ed68960bcabbe9ec3109ac232b3f41f9a01a49ef48f"

#方案2对I加密
def jm2(i, e, f):
    e=int(e,16)
    f=int(f,16)
    i=i[::-1]  #网易多搞的一步 star:stop:step 字符串反转 stop默认0 stop默认-1
    print(i)
    bs=i.encode("utf-8")
    s=binascii.b2a_hex(bs).decode()
    s=int(s,16)
    mi=(s**e)%f
    # print(format(mi,"x"))
    return format(mi,"x")

def asrsea(data,a,e,f):
    i="xS5OktXaZxEDoVUb"
    #加密data
    key=f #用于第一次加密的参数
    data1=jm1(data,key,b"0102030405060708")
    data2=jm1(data1,i,b"0102030405060708")
    jmhd_i=jm2(i, a, e)
    return data2,jmhd_i

if __name__ == '__main__':
    # https://music.163.com/#/song?id=2099625932
    u = input("请输入歌曲链接:")
    gqid=u.split("=")[-1]
    # 请求参数加密前的样子:
    data = {
        "encodeType": "aac",
        "ids": [gqid],
        "level": "standard",
        "csrf_token": ""
    }
    #把参数转成请求需要的json格式,浏览器端加密的时候参数也是json格式的
    data = json.dumps(data)

    #复制固定值传值给asrsea加密
  cs1,cs2=asrsea(data,'010001','00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7','0CoJUm6Qyw8W8jud')
    #加密后参数给dict
    dict={
      "params":cs1,
      "encSecKey":cs2}
      #发请求
    UA={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"}
    resp=requests.post(url="https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=",data=dict,headers=UA)
    dict = resp.json()
    for i in dict["data"]:
      url = i.get("url")
      resp = requests.get(url=url,headers=UA)
      with open(url.split("/")[-1],mode="wb") as f:
          f.write(resp.content)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋刀鱼_(:з」∠)_别急

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值