延续上一篇的文章,今天我想聊聊网易云音乐的接口加密算法(先打开js的项目工程,谢谢https://github.com/Binaryify/NeteaseCloudMusicApi)
随便搜了一下代码,比如login,基本能锁定两个比较重要的文件,request.js和crypto.js
crytpo.js用来封装加密算法,也是请求部分的核心之一,我要做的其实很简单,用Java实现里面js功能。
下面是js的登录代码封装
// 手机登录
const crypto = require('crypto')
module.exports = (query, request) => {
query.cookie.os = 'pc'
const data = {
phone: query.phone,
countrycode: query.countrycode,
password: crypto.createHash('md5').update(query.password).digest('hex'),
rememberLogin: 'true'
}
console.log(data);
return request(
'POST', `https://music.163.com/weapi/login/cellphone`, data,
{crypto: 'weapi', ua: 'pc', cookie: query.cookie, proxy: query.proxy}
)
}
要注意几个细节:
1、query.cookie.os = 'pc',在Java里面其实就是在请求的cookie里面加上"oc","pc"(用hashmap设置到"Cookie"里面,然后一起封装到header里面)
2、对密码做MD5加密,这个没啥难度,通用加密方式,如果你要偷懒也行
package music.netease.com.neteasecloudmusic.utils;
import java.security.MessageDigest;
public class MD5Utils {
public final static String MD5(String s) {
char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f' };
try {
byte[] btInput = s.getBytes();
// 获得MD5摘要算法的 MessageDigest 对象
MessageDigest mdInst = MessageDigest.getInstance("MD5");
// 使用指定的字节更新摘要
mdInst.update(btInput);
// 获得密文
byte[] md = mdInst.digest();
// 把密文转换成十六进制的字符串形式
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
return null;
}
}
}
MD5加密后的结果最好验证一下,对比一下js和Java加密后的打印数据
3、接着可以直接跳到request.js那个问题,里面封装了最终的网络请求,debug或者log能很快定位到参数处理的位置,当然,如果代码感觉好可以直接看到
if (options.crypto == 'weapi') {
let csrfToken = (headers['Cookie'] || '').match(/_csrf=([^(;|$)]+)/)
console.log(csrfToken);
data.csrf_token = csrfToken ? csrfToken[1] : ''
data = encrypt.weapi(data)
console.log('weapi:'+queryString.stringify(data));
url = url.replace(/\w*api/, 'weapi')
} else if (options.crypto == 'linuxapi') {
data = encrypt.linuxapi({
method: method,
url: url.replace(/\w*api/, 'api'),
params: data
})
headers['User-Agent'] =
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36'
url = 'https://music.163.com/api/linux/forward'
}
登陆请求的option.crypto是weapi,请求的参数都会以键值对的方式放到map中,然后转成jsonObject调用Encrypt,看return就知道最终返回的还是个map
js:
function Encrypt(obj) {
var text = JSON.stringify(obj);
console.log(text);
var secKey = createSecretKey(16)
console.log(secKey);
var encText = aesEncrypt(aesEncrypt(text, nonce), secKey);
var encSecKey = rsaEncrypt(secKey, pubKey, modulus);
return {
params: encText,
encSecKey: encSecKey
}
}
对应的Java:
public static Map<String, String> encrypt(String text) {
String secKey = getRandomString(16);
// String encText = aesEncrypt(aesEncrypt(text, nonce), secKey);
String encText = aesEncrypt(aesEncrypt(text, nonce), secKey);
String encSecKey = rsaEncrypt(secKey, pubKey, modulus);
Map<String, String> map = new HashMap<String, String>();
map.put(PARAMS, encText);
map.put(ENCSECKEY, encSecKey);
return map;
}
这部分的代码大致意思应该是,把传进来的参数转成字符串赋值给text,Java我就用string,randomByte(16)就很简单了,获取一个16位的随机数(从26个字母大小写+0~9这10个数字),赋值给secretKey,然后再这两个参数做两次aes加密,一次rsa加密。
随机数的处理我在Java里面单独写了一个方法,不知道为啥js可以这么简洁,我一定要找时间学一下
/*获取由字母和数字组成的随机数*/
static private String getRandomString(int length){
String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random=new Random();
StringBuffer sb=new StringBuffer();
for(int i=0;i<length;i++){
int number=random.nextInt(62);
sb.append(str.charAt(number));
}
return sb.toString();
}
str对应了js代码里面的base62那个变量,aes加密需要传入text和seckey
js:
function aesEncrypt(text, secKey) {
var _text = text;
var lv = new Buffer('0102030405060708', "binary");
var _secKey = new Buffer(secKey, "binary");
var cipher = crypto.createCipheriv('AES-128-CBC', _secKey, lv);
var encrypted = cipher.update(_text, 'utf8', 'base64');
encrypted += cipher.final('base64');
return encrypted;
}
java:
private static String aesEncrypt(String text,String mode,String key,IvParameterSpec iv){
try {
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
byte[] encrypted = cipher.doFinal(text.getBytes());
return Base64Encoder.encode(encrypted);
} catch (Exception e) {
return "";
}
}
没啥好说的,就按照原装的写法,百度,弄出一个类似的表达式,把参数对应上,有一个关键的字段nonce,应该是秘钥,第一次aes加密用text(也就是参数转化的json)和nonce,再将加密后的结果和那个16位的随机数(随机秘钥)穿进去生成最终的encText。最终的encSecKey通过rsa加密实现
js:
function rsaEncrypt(text, pubKey, modulus) {
var _text = text.split('').reverse().join('');
var biText = bigInt(new Buffer(_text).toString('hex'), 16),
biEx = bigInt(pubKey, 16),
biMod = bigInt(modulus, 16),
biRet = biText.modPow(biEx, biMod);
return zfill(biRet.toString(16), 256);
}
java:
private static String rsaEncrypt(String text, String pubKey, String modulus) {
text = new StringBuilder(text).reverse().toString();
BigInteger rs = new BigInteger(String.format("%x", new BigInteger(1, text.getBytes())), 16)
.modPow(new BigInteger(pubKey, 16), new BigInteger(modulus, 16));
String r = rs.toString(16);
if (r.length() >= 256) {
return r.substring(r.length() - 256, r.length());
} else {
while (r.length() < 256) {
r = 0 + r;
}
return r;
}
}
text就是那个16位的随机数,pubkey和modulus是抓包拿到的,js文件里面有。最后将两个参数封装到map里面返回出去。
不要看到js代码的接口都是get,其实那是因为大神在底层已经封装了一层,所有的接口都能用post的方式实现,还有就是很多接口的请求地址并非是大神提供的那些,你要好好接口文档,或者学我直接调试看日志。加密后的map最后转化成formbody,然后用post的方式去请求,完工!至于大神提到的很多head cookies,我跑了一下Java控制台debug了一下,基本都不用带。对于js加密转Java加密,我就介绍到这里,有啥不理解的欢迎评论留言,Android版本的网易云音乐包括登录只实现了5个接口,获取用户歌单、歌单的歌曲列表、收藏的MV,视频播放链接获取。然后加上了ijkPlayer库,实现基本的视频播放功能,由于页面太low,功能太少就暂时不开放了,争取月底完成音乐播放功能,到时候将所有代码开放到GitHub上,需要技术交流的话可以私下沟通。
网友评论