前言
缺点:不能够爬会员歌曲
网易云音乐网页的源代码里没有下载歌曲的url,开发者工具里也无法在浏览器渲染后的页面代码里找到,所以–进行抓包。
抓包发现目标。
访问一下url
一、分析请求
抓到的请求为Post请求,有两个加密的参数params和encSecKey。
二、探索加密的方法
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处理字节时3个3个一起处理,处理成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)