网易云音乐API分析

环境:Win10
工具:Fiddler 4,Chrome浏览器

想做一个网易云音乐的下载地址解析,于是有了这篇文章,记录下过程。
开Fiddler截包
抓取到如下的包

POST http://music.163.com/weapi/song/enhance/player/url?csrf_token= HTTP/1.1
Host: music.163.com
Connection: keep-alive
Content-Length: 412
Origin: http://music.163.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Referer: http://music.163.com/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8
Cookie: _ntes_nnid=3ddf2945dfda076e60b4ee0162f3c0cd,1488291659575; _ntes_nuid=3ddf2945dfda076e60b4ee0162f3c0cd; vjuids=76bd0b1bc.15acc1b713d.0.73360b94f7956; vjlast=1489483035.1489483035.30; vinfo_n_f_l_n3=e441d5bafac7c9b7.1.0.1489483034993.0.1489483124982; JSESSIONID-WYYY=m%2FO4x3rjCh4e1xfBjmOCZ52hbV7rD7M9U77ZX9qn%2BMw5WeKyU0vnw1zGmpbn%5Ci5ZWmdaRmVjQromGw%2BxForePwG3mBf6jOy27vj1IMOv%5ClM3%2BXkUrPOeM7qPP9HhgO%2F%2Fd%5C1nWUq3mDUIicmtDvWMsUbEeWS%5CbNAJyUO8t%5CbHCy731FHp%3A1489661205597; _iuqxldmzr_=32; __utma=94650624.1301580310.1488291660.1489644772.1489659406.9; __utmb=94650624.4.10.1489659406; __utmc=94650624; __utmz=94650624.1489581581.6.4.utmcsr=baidu|utmccn=(organic)|utmcmd=organic

params=TsOlwX6jVxTvkYi3mFgJlVqczMblRwfL6sWG2wcRkYr5I2pB1GSC3eH4tLquNlPiXrBotId2T%2FI4UEwjRvbh3L%2FrlBKQ9de1vohxzueBBv%2FM01D4Fa%2BXhJ7jrSIhj%2FD2&encSecKey=747e088d0d3867619c340da1bedba0e60265ece84c0c1ef5ae81eff317b4afdaeed3666a9e1c3628abcabc0c602dba7685e1dfcc7ef82b6be24c35588a39941b6fe8e2a6fa751fded9c9b61c1cbf604d21b1cde54f244ea25c4db6673ef76151b8069b0ed5323c9ec3d2bace24b073b53e408ebb445c76a044ac797fa7c13be0

再看返回数据

HTTP/1.1 200 OK
Server: nginx
Date: Thu, 16 Mar 2017 10:19:35 GMT
Content-Type: text/plain;charset=UTF-8
Connection: keep-alive
Vary: Accept-Encoding
Cache-Control: no-store
Pragrma: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: no-cache
Content-Length: 352

{"data":[{"id":426881487,"url":"http://m10.music.126.net/20170316184438/37a51d1b5257f895cd867ff2dbd40186/ymusic/cba9/2013/eacb/50ab2466dcd90414959d84aa9c65dd9f.mp3","br":128000,"size":4575025,"md5":"50ab2466dcd90414959d84aa9c65dd9f","code":200,"expi":1200,"type":"mp3","gain":-3.1099,"fee":0,"uf":null,"payed":0,"flag":0,"canExtend":false}],"code":200}

返回的json中包含了mp3的真实地址
这里看看发包的数据

params=TsOlwX6jVxTvkYi3mFgJlVqczMblRwfL6sWG2wcRkYr5I2pB1GSC3eH4tLquNlPiXrBotId2T%2FI4UEwjRvbh3L%2FrlBKQ9de1vohxzueBBv%2FM01D4Fa%2BXhJ7jrSIhj%2FD2&encSecKey=747e088d0d3867619c340da1bedba0e60265ece84c0c1ef5ae81eff317b4afdaeed3666a9e1c3628abcabc0c602dba7685e1dfcc7ef82b6be24c35588a39941b6fe8e2a6fa751fded9c9b61c1cbf604d21b1cde54f244ea25c4db6673ef76151b8069b0ed5323c9ec3d2bace24b073b53e408ebb445c76a044ac797fa7c13be0

看不出任何的规律,故猜测是加密的数据,一般而言都是通过js进行加密的,那我们就去js里找一找
在浏览器中按F12调出开发者工具看到有一个core.js,打开看一看
通过搜索关键词params可以找到如下代码

var bua = window.asrsea(JSON.stringify(bl), bbZ(["流泪", "强"]), bbZ(cnb.md), bbZ(["爱心", "女孩", "惊恐", "大笑"]));
bf.data = bm.eX({
        params: bua.encText,
        encSecKey: bua.encSecKey
    })

这里看到paramsencSecKey是通过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
    }
    function e(a, b, d, e) {
        var f = {};
        return f.encText = c(a + e, b, d),
        f
    }
    window.asrsea = d,
    window.ecnonasr = e
}

这里看到asrsea函数就是d函数,并且传入了4个参数
对四个参数进行分析
利用fiddler的AutoResponder可以方便我们本地调试js
在core.js中加入这段

window.console.info(bl);
var bua = window.asrsea(JSON.stringify(bl), bbZ(["流泪", "强"]), bbZ(cnb.md), bbZ(["爱心", "女孩", "惊恐", "大笑"]));
window.console.warn(bua.encText);
bf.data = bm.eX({
        params: bua.encText,
        encSecKey: bua.encSecKey
    })

清除浏览器的缓存,刷新页面,可以看到开发者工具console里出现了信息
{“ids”:”[426881487]”,”br”:128000,”csrf_token”:”“}
这个是参数一
用同样的方法获取到四个参数

参数一
{"ids":"[426881487]","br":128000,"csrf_token":""}
参数二
010001
参数三
00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
参数四
0CoJUm6Qyw8W8jud

显然encTextparams是等值的,那看一下encText是如何计算的

    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
    }

调用了b方法,再去看看b

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

这是一个CBC模式的AES加密,偏移量是 “0102030405060708”
第一个参数是被加密文本
第二个参数是定值0CoJUm6Qyw8W8jud
回到b方法

    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
    }

encText进行了两次加密,第二次是把第一次加密的结果再加密一次
那么第二次加密的key值i是怎么获得的,可以看到调用了a方法,再去看看a

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
    }

这是一个取随机字符的方法,参数决定了取出多少位,既然是随机数我们就可以自己随便写,只要是16位即可
至此我们分析出了params的加密方式
再来看看encSecKey

h.encSecKey = c(i, e, f),

调用了c方法,但是c方法传递的参数是固定的(i由我们自己决定),那么这个encSecKey也就是固定的

至此两个参数是如何得到的我们都分析出来了

本篇参考了知乎,点此跳转


附Java计算params值代码

/**
 * Created by PVer on 2017/3/19.
 */

import java.util.Scanner;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import java.net.URLEncoder;

public class AES {
    // 加密
    public static String Encrypt(String sSrc, String sKey) throws Exception {
        if (sKey == null) {
            System.out.print("Key为空null");
            return null;
        }
        // 判断Key是否为16位
        if (sKey.length() != 16) {
            System.out.print("Key长度不是16位");
            return null;
        }
        byte[] raw = sKey.getBytes();
        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");//"算法/模式/补码方式"
        IvParameterSpec iv = new IvParameterSpec("0102030405060708".getBytes());//使用CBC模式,需要一个向量iv,可增加加密算法的强度
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
        byte[] encrypted = cipher.doFinal(sSrc.getBytes());

        return new BASE64Encoder().encode(encrypted);//此处使用BASE64做转码功能,同时能起到2次加密的作用。
    }

    // 解密
    public static String Decrypt(String sSrc, String sKey) throws Exception {
        try {
            // 判断Key是否正确
            if (sKey == null) {
                System.out.print("Key为空null");
                return null;
            }
            // 判断Key是否为16位
            if (sKey.length() != 16) {
                System.out.print("Key长度不是16位");
                return null;
            }
            byte[] raw = sKey.getBytes("UTF-8");
            SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            IvParameterSpec iv = new IvParameterSpec("0102030405060708"
                    .getBytes());
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
            byte[] encrypted1 = new BASE64Decoder().decodeBuffer(sSrc);//先用base64解密
            try {
                byte[] original = cipher.doFinal(encrypted1);
                String originalString = new String(original);
                return originalString;
            } catch (Exception e) {
                System.out.println(e.toString());
                return null;
            }
        } catch (Exception ex) {
            System.out.println(ex.toString());
            return null;
        }
    }

    public static String get_params(String text) throws Exception {
        String first_key = "0CoJUm6Qyw8W8jud";
        String second_key = "FFFFFFFFFFFFFFFF";
        String h_encText = AES.Encrypt(text, first_key);
        h_encText = AES.Encrypt(h_encText, second_key);
        return h_encText;
    }

    public static String get_encSecKey() {
        String encSecKey = "257348aecb5e556c066de214e531faadd1c55d814f9be95fd06d6bff9f4c7a41f831f6394d5a3fd2e3881736d94a02ca919d952872e7d0a50ebfa1769a7a62d512f5f1ca21aec60bc3819a9c3ffca5eca9a0dba6d6f7249b06f5965ecfff3695b54e1c28f3f624750ed39e7de08fc8493242e26dbc4484a01c76f739e135637c";
        return encSecKey;
    }

    public static void main(String[] args) throws Exception {
        /*
         * 加密用的Key 可以用26个字母和数字组成,最好不要用保留字符,虽然不会错,至于怎么裁决,个人看情况而定
         * 此处使用AES-128-CBC加密模式,key需要为16位。
         */
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入要解析的音乐ID:");
        String first_param = "{\"ids\":\"[" + scanner.next() + "]\",\"br\":128000,\"csrf_token\":\"\"}";
        System.out.println("params=" + URLEncoder.encode(AES.get_params(first_param), "UTF-8") + "&encSecKey=" + AES.get_encSecKey());
    }
}

你可能感兴趣的:(post)