延續(xù)上一篇的文章趾断,今天我想聊聊網(wǎng)易云音樂的接口加密算法(先打開js的項目工程,謝謝https://github.com/Binaryify/NeteaseCloudMusicApi)
隨便搜了一下代碼增显,比如login脐帝,基本能鎖定兩個比較重要的文件,request.js和crypto.js
crytpo.js用來封裝加密算法炸站,也是請求部分的核心之一疚顷,我要做的其實很簡單,用Java實現(xiàn)里面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}
)
}
要注意幾個細節(jié):
1忌堂、query.cookie.os = 'pc',在Java里面其實就是在請求的cookie里面加上"oc"枷遂,"pc"(用hashmap設(shè)置到"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");
// 使用指定的字節(jié)更新摘要
mdInst.update(btInput);
// 獲得密文
byte[] md = mdInst.digest();
// 把密文轉(zhuǎn)換成十六進制的字符串形式
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加密后的結(jié)果最好驗證一下,對比一下js和Java加密后的打印數(shù)據(jù)
3证薇、接著可以直接跳到request.js那個問題匆篓,里面封裝了最終的網(wǎng)絡(luò)請求,debug或者log能很快定位到參數(shù)處理的位置箩张,當然窗市,如果代碼感覺好可以直接看到
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咨察,請求的參數(shù)都會以鍵值對的方式放到map中,然后轉(zhuǎn)成jsonObject調(diào)用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;
}
這部分的代碼大致意思應該是誉券,把傳進來的參數(shù)轉(zhuǎn)成字符串賦值給text刊愚,Java我就用string,randomByte(16)就很簡單了商玫,獲取一個16位的隨機數(shù)(從26個字母大小寫+0~9這10個數(shù)字)牡借,賦值給secretKey,然后再這兩個參數(shù)做兩次aes加密炬藤,一次rsa加密碴里。
隨機數(shù)的處理我在Java里面單獨寫了一個方法,不知道為啥js可以這么簡潔羹膳,我一定要找時間學一下
/*獲取由字母和數(shù)字組成的隨機數(shù)*/
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 "";
}
}
沒啥好說的寇壳,就按照原裝的寫法,百度图贸,弄出一個類似的表達式冕广,把參數(shù)對應上撒汉,有一個關(guān)鍵的字段nonce,應該是秘鑰睬辐,第一次aes加密用text(也就是參數(shù)轉(zhuǎn)化的json)和nonce,再將加密后的結(jié)果和那個16位的隨機數(shù)(隨機秘鑰)穿進去生成最終的encText侵俗。最終的encSecKey通過rsa加密實現(xiàn)
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位的隨機數(shù)隘谣,pubkey和modulus是抓包拿到的寻歧,js文件里面有。最后將兩個參數(shù)封裝到map里面返回出去码泛。
不要看到j(luò)s代碼的接口都是get猾封,其實那是因為大神在底層已經(jīng)封裝了一層,所有的接口都能用post的方式實現(xiàn)噪珊,還有就是很多接口的請求地址并非是大神提供的那些晌缘,你要好好接口文檔,或者學我直接調(diào)試看日志卿城。加密后的map最后轉(zhuǎn)化成formbody枚钓,然后用post的方式去請求,完工瑟押!至于大神提到的很多head cookies搀捷,我跑了一下Java控制臺debug了一下,基本都不用帶多望。對于js加密轉(zhuǎn)Java加密嫩舟,我就介紹到這里怀偷,有啥不理解的歡迎評論留言家厌,Android版本的網(wǎng)易云音樂包括登錄只實現(xiàn)了5個接口,獲取用戶歌單椎工、歌單的歌曲列表饭于、收藏的MV,視頻播放鏈接獲取维蒙。然后加上了ijkPlayer庫掰吕,實現(xiàn)基本的視頻播放功能,由于頁面太low颅痊,功能太少就暫時不開放了殖熟,爭取月底完成音樂播放功能,到時候?qū)⑺写a開放到GitHub上斑响,需要技術(shù)交流的話可以私下溝通菱属。