分析网易云音乐接口修改用户Model
1.背景
由**Binaryify/NeteaseCloudMusicApi**项目获得启发,想要通过django做一个网易云音乐API作为联系项目。想要实现这个目的,首先要坐的就是要分析网易云音乐的接口规则。
2.网易云音乐登记接口分析
2.1.手机号登陆接口分析
经过分析,其中的https://music.163.com/weapi/login/cellphone?csrf_token=
请求URL即为手机号登陆接口URL。
其中该接口请求方式为POST
方式,返回报文格式为json
格式,同时会设置三个cookie
。初步设想可以通过requests
库模拟HTTPS
的POST
请求,获取到返回结果后,将获取到的cookie
信息设置到浏览器的本地cookie
中,或者保存到本地数据库中,可以实现不同接口之间的cookies
数据的持续化存储。
而对于请求,则是From
表单方式发送,总共有两个参数:params
、encSecKey
,可以看到均是加密数据,所以需要继续分析请求数据的加密方式。
通过Network
请求信息中的Initiator
参数可以看出,该请求的处理逻辑在core*.js
的js文件中,所以可以查看其源码。
源码为压缩版的js,可以通过编辑器进行代码格式化(这里使用vscode进行格式化),格式化后找到相关代码段。
可以看出两个参数都是通过window.asrsea
函数进行处理,因此通过该函数可以查询到该函数具体的实现过程。
通过抓包工具fiddler,将本地的core*.js文件替代网页加载的js文件。
将传递的四个参数打印出来
发起手机登陆交易
查看浏览器调试工具中后台打印日志
与其他请求对比,发现其中第一个参数为请求数据,后三个参数均为固定参数,然后再去观察js代码处理函数。
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
}
其中d
为请求参数,e
为010001
,f
为00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
,g
为0CoJUm6Qyw8W8jud
。
参数i为函数a
返回值,函数a
处理逻辑为以abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
为种子的16位随机字符串。
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
}
h.encText
参数为函数b
的返回值,其中a=d=请求参数
,b=g='0CoJUm6Qyw8W8jud'
,即对请求参数a
进行加密,加密密钥为b='0CoJUm6Qyw8W8jud'
,密钥偏移量IV为0102030405060708
,加密算法为CBC模式。之后h.encText = b(h.encText, i)
对已加密的字符串进行二次加密,加密密钥为16为随机数i
,密钥偏移量IV、加密算法均不变。
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()
}
h.encSecKey
参数为函数c
的返回值,其中a=i=16位随机数
,b=e='010001'
,c=f='00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
,即对参数第二次加密时使用的加密密钥“16位随机数”进行RSA加密,通过16进制e值b='010001'
、公钥c
生成RSA加密密钥。
function c(a, b, c) {
var d, e;
return setMaxDigits(131),
d = new RSAKeyPair(b, "", c),
e = encryptedString(d, a)
}
2.2. 根据分析结果编写python版加密处理
根据以上分析,编写python版的加密代码apps/utils/encrypt.py
,对于RSA算法的加密算法公式如下:
c
=
m
e
(
m
o
d
n
)
c = m^e (mod n)
c=me(modn)
解密公式如下:
m
=
c
d
(
m
o
d
n
)
m = c^d (mod n)
m=cd(modn)
其中,加密时的公钥中,n为两个素数p和q的乘积(p和q必须保密),e与(p-1)(q-1)互质。
注:
- 代码中用到加密三方库pycryptodome,对于pycrypto库已停止更新,所以使用pycryptodome作为替代。
pip install pycryptodome
-
代码中以
__
双下划线开头的参数,均为私有属性,不能在类的外部被使用或直接访问。 -
代码中
__
双下划线开头的方法,均为私有方法,不能在类的外部被调用。
apps/utils/common.py
代码如下:
import random
def random_string(length=6):
"""获取特定长度的随机字符串"""
seed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return ''.join(random.sample(seed, int(length)))
apps/utils/encrypt.py
代码如下:
import json
import base64
import binascii
from Crypto.Cipher import AES
from apps.utils.common import random_string
class APIEncrypt(object):
"""API接口加密类"""
def __init__(self):
"""初始化函数
__AES_MODE_CBC:AES加密CBC模式
__AES_MODE_ECB:AES加密CEB模式
__AES_IV:AES加密偏移量
__AES_NONCE:AES加密一次性密钥
__AES_SECRET_KEY:AES加密密钥
__AES_LINUX_API_KEY:linux相关API的AES加密密钥
__RSA_PUBLIC_KEY:RSA加密公钥
__RSA_MODULUS:RSA加密模量
"""
self.__AES_MODE_CBC = AES.MODE_CBC
self.__AES_MODE_ECB = AES.MODE_ECB
self.__AES_IV = '0102030405060708'
self.__AES_NONCE = '0CoJUm6Qyw8W8jud'
self.__AES_SECRET_KEY = random_string(16)
self.__AES_LINUX_API_KEY = 'rFgB&h#%2?^eDg:Q'
self.__RSA_PUBLIC_KEY = '010001'
self.__RSA_MODULUS = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
def __encrypt_aes(self, params, secret_key, aes_mode=AES.MODE_CBC, iv='', result_coding='base64'):
"""AES加密处理
"""
# ① 请求参数补位处理
pad = 16 - len(params) % 16
params = params + chr(pad) * pad
# ② 创建加密器,其中加密模式默认为CBC
encryptor = AES.new(secret_key.encode('utf8'), aes_mode, iv.encode('utf8')) if iv else \
AES.new(secret_key.encode('utf8'), self.__AES_MODE_ECB)
# ③ 对参数进行加密处理
cipher_params = encryptor.encrypt(params.encode('utf-8'))
# ④ 对加密后的结果进行编码处理,默认base64编码,否则为十六进制编码
if result_coding == 'base64':
cipher_params = base64.b64encode(cipher_params).decode('utf-8')
elif result_coding == 'hex':
cipher_params = binascii.hexlify(cipher_params)
return cipher_params
def __encrypt_rsa(self):
"""RSA加密"""
# ① 将加密密钥倒置
aes_secret_key = self.__AES_SECRET_KEY[::-1]
# ② 加密处理
rs = pow(int(binascii.hexlify(aes_secret_key.encode('utf8')), 16),
int(self.__RSA_PUBLIC_KEY, 16),
int(self.__RSA_MODULUS, 16))
# ③ 将加密结果转成十六进制编码,并右对齐前补零,补齐256位
return format(rs, 'x').zfill(256)
def encrypt_weapi(self, params):
"""web版API加密处理"""
# ① 两次加密处理:
# ①-1 对请求参数进行AES加密,加密密钥为__AES_NONCE,加密模式为CBC,加密偏移量为__AES_IV
# ①-2 对机密结果再次加密,加密密钥为__AES_SECRET_KEY(十六位随机字符串),加密模式为CBC,加密偏移量为__AES_IV
params = json.dumps(params)
encrypt_params = self.__encrypt_aes(
self.__encrypt_aes(params, self.__AES_NONCE, aes_mode=self.__AES_MODE_CBC, iv=self.__AES_IV),
self.__AES_SECRET_KEY,
aes_mode=self.__AES_MODE_CBC,
iv=self.__AES_IV
)
# ② 对AES加密使用到的密钥__AES_SECRET_KEY(十六位随机字符串)进行RSA加密处理
encrypt_secret_key = self.__encrypt_rsa()
return {'params': encrypt_params, 'encSecKey': encrypt_secret_key}
def encrypt_linuxapi(self, params):
"""linux版API加密处理"""
# ① 直接对请求参数进行AES加密,加密密钥为__AES_LINUX_API_KEY,加密模式为ECB,最终编码为十六进制
params = json.dumps(params)
encrypt_params = self.__encrypt_aes(
params, self.__AES_LINUX_API_KEY, aes_mode=self.__AES_MODE_ECB, result_coding='hex')
return {'eparams': encrypt_params.upper()}