Logo

网易云音乐直链解析

Avatar

Skyone

科技爱好者

本文详细介绍了提取网易云音乐前端歌曲播放链接的分析过程。

重要

在写这篇文章时刚刚学习前端不久,代码写的比较烂……总之重点在如何破解网易云音乐的加密方式。

后面有一篇博文展示了如何写一个真正可用的 API ,但由于本博客正在搬迁,搬运完成后会在这里把链接贴出来。

技术栈

  • Koa基础

推荐一篇博客,把koa讲的非常易懂,几乎看懂他提供的例子后koa就会得差不多了,如果有时间,我可能也会写一篇教程。

  • JavaScript基础
  • HTML基础

用到的Node.js模块

  • koa
  • koa-route
  • axios
  • crypto-js

如果你用的WebStorm,直接写

const Koa = require("koa");
const querystring = require("querystring");
const CryptoJS = require("crypto-js");
const axios = require("axios");
const fs = require("fs");
const route = require("koa-route");
const app = new Koa();

即可,WebStorm会自动提示你安装。

其实自己安装也很简单,先切换到工作目录,用cmd或shell运行:

npm install koa
npm install koa-route
npm install axios
npm install crypto-js

实现

1. 抓包、定位加密代码

首先打开网易云音乐网页版,进入任意一首歌,打开浏览器的DevTools,选择Network,点击播放,稍加分析,不难看出,这个post请求是用来获取音乐链接。

抓包

切换到调用栈(Initiator),看看发送它的函数在哪

调用栈

打开,发现是一大坨看不懂的代码

一大坨不想看的代码

看来这样走不通,那就搜搜post请求的data吧,Ctrl+F,搜encSecKey

请求头

定位加密代码

嗯,完美,很显然,这里的两个参数来自第13297行(可能你看到的行数和我不一样)的window.asrsea()函数

先刷新一下,再在那一行打个断点,点击播放

断点触发,进入window.asrsea()函数

进入加密函数

再在那个d(d,e,f,g)函数的第一行打个断点,可以看到这就是我们要找的加密函数。

2. 分析加密代码

加密代码

在左边的局部变量中看出,d保存的是一个字符串化的json,保存着要获取的歌曲id

{
  "id": [
    32102297
  ],
  "level": "standard",
  "encodeType": "aac",
  "csrf_token": ""
}

经过多次测试,e是一个定值:"010001",来自["流泪", "强"]两个表情转换为对应的代码,转换映射如下:

{
  "色": "00e0b", "流感": "509f6", "这边": "259df", "弱": "8642d",
  "嘴唇": "bc356", "亲": "62901", "开心": "477df", "呲牙": "22677",
  "憨笑": "ec152", "猫": "b5ff6", "皱眉": "8ace6", "幽灵": "15bb7",
  "蛋糕": "b7251", "发怒": "52b3a", "大哭": "b17a8", "兔子": "76aea",
  "星星": "8a5aa", "钟情": "76d2e", "牵手": "41762", "公鸡": "9ec4e",
  "爱意": "e341f", "禁止": "56135", "狗": "fccf6", "亲亲": "95280",
  "叉": "104e0", "礼物": "312ec", "晕": "bda92", "呆": "557c9",
  "生病": "38701", "钻石": "14af6", "拜": "c9d05", "怒": "c4f7f",
  "示爱": "0c368", "汗": "5b7a4", "小鸡": "6bee2", "痛苦": "55932",
  "撇嘴": "575cc", "惶恐": "e10b4", "口罩": "24d81", "吐舌": "3cfe4",
  "心碎": "875d3", "生气": "e8204", "可爱": "7b97d", "鬼脸": "def52",
  "跳舞": "741d5", "男孩": "46b8e", "奸笑": "289dc", "猪": "6935b",
  "圈": "3ece0", "便便": "462db", "外星": "0a22b", "圣诞": "8e7",
  "流泪": "01000", "强": "1", "爱心": "0CoJU", "女孩": "m6Qyw",
  "惊恐": "8W8ju", "大笑": "d"
}

f同样是定值,来自一下表情转换为代码

[
  "", "流感", "这边", "", "嘴唇", "", "开心", "呲牙", "憨笑",
  "", "皱眉", "幽灵", "蛋糕", "发怒", "大哭", "兔子", "星星", "钟情",
  "牵手", "公鸡", "爱意", "禁止", "", "亲亲", "", "礼物", "",
  "", "生病", "钻石", "", "", "示爱", "", "小鸡", "痛苦",
  "撇嘴", "惶恐", "口罩", "吐舌", "心碎", "生气", "可爱", "鬼脸",
  "跳舞", "男孩", "奸笑", "", "", "便便", "外星", "圣诞"
]

g同上,是["爱心", "女孩", "惊恐", "大笑"]转换为代码

所以

e = "010001"
f = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
g = "0CoJUm6Qyw8W8jud"

嗯,常量搞清楚了,再看看加密方法

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

enText就是post请求里的params参数,来自b函数加密两次

encSecKey来自c函数加密一次

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
}

显然,用来生成指定长度的随机字符串

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

只使用了CryptoJS的函数,由于我们也用JavaScript写代码,直接复制即可,管他干嘛的-_-,只要在开头加个

const CryptoJS = require("crypto-js")

即可。

c函数

function c(a, b, c) {
    var d, e;
    return setMaxDigits(131),
        d = new RSAKeyPair(b, "", c),
        e = encryptedString(d, a)
}

看着很短,实际上调用了自定义的类,这样就不能用对付b函数的方法了,这里我们先不去看c函数干了什么

我们看看它的参数

c函数的参数d函数里对应的变量
ai长度位16的随机字符串
be"010001"
cf"00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"

站在网易的角度想想,向网易传了一串加密后的字符串,这个字符串经过一个随机数和三个常量的加密,要想解密,必然需要那个随机数,而encSecKey显然是用来提供那个随机数的,encSecKey只来自c函数且c函数接受一个随机数和两个常量,可以解密出那个随机数,因此:

如果那个a函数得到的随机数如果我们用定值代替,嘿嘿嘿,c函数得出的encSecKey也将是定值!

我们同调试函数即可获取一个随机数和与之对应的加密后的encSecKey

我获得的是:

i = "bEjJE2aqLOyTEZiv"
encSecKey = "3e7ad1dbe03a65fc32268930314b88bcbfc1e9782c3b398c30b62776e39b66a048a7122d282a13d99f9b63bd4e1940b136169fbedf56c1887933fa59a01f95c4c0e78a6d9bb7f91605408e9c1c3c2e57873c53cdf09a3d79a43cfe26260741097089e4bd19808aab395190274e687b807ffddee89f39d75f2288e28a582f3d08"

写代码

经过上面的分析,我们可以得出如下代码:

const CryptoJS = require("crypto-js")

function b(a, b) {
    const 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 maker(d) {
    var h = {}
      , i = "bEjJE2aqLOyTEZiv";
    h.encText = b(d, "0CoJUm6Qyw8W8jud");
    h.encText = b(h.encText, i);
    h.encSecKey = "3e7ad1dbe03a65fc32268930314b88bcbfc1e9782c3b398c30b62776e39b66a048a7122d282a13d99f9b63bd4e1940b136169fbedf56c1887933fa59a01f95c4c0e78a6d9bb7f91605408e9c1c3c2e57873c53cdf09a3d79a43cfe26260741097089e4bd19808aab395190274e687b807ffddee89f39d75f2288e28a582f3d08";
    return [h.encText, h.encSecKey];
}

是不是很简单?

3. 实现后端

这部分没什么可讲的,要讲的话也只是将web,因此,跳过。。

完整代码如下:

app.js

const Koa = require("koa")
const querystring = require("querystring")
const axios = require('axios')
const fs = require("fs");
const route = require('koa-route');
const app = new Koa();
const CryptoJS = require("crypto-js")

function b(a, b) {
    const 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 maker(d) {
    var h = {}
      , i = "bEjJE2aqLOyTEZiv";
    h.encText = b(d, "0CoJUm6Qyw8W8jud");
    h.encText = b(h.encText, i);
    h.encSecKey = "3e7ad1dbe03a65fc32268930314b88bcbfc1e9782c3b398c30b62776e39b66a048a7122d282a13d99f9b63bd4e1940b136169fbedf56c1887933fa59a01f95c4c0e78a6d9bb7f91605408e9c1c3c2e57873c53cdf09a3d79a43cfe26260741097089e4bd19808aab395190274e687b807ffddee89f39d75f2288e28a582f3d08";
    return [h.encText, h.encSecKey];
}

const page = (ctx) => {
    ctx.response.type = "html";
    ctx.response.body = fs.createReadStream("./index.html");
}
const request = async (id) => {
    const answer = {status: 500, body: {}}
    let params = maker(JSON.stringify({
        ids: [id],
        level: "standard",
        encodeType: "aac",
        csrf_token: ""
    }));
    const settings = {
        method: "post",
        url: "https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=",
        data: querystring.stringify({
            params: params[0],
            encSecKey: params[1]
        })
    };
    await axios(settings)
        .then(res => {
            answer.body = res.data;
            answer.status = true;
        })
        .catch((err => {
            answer.body = err;
            answer.status = false;
        }));
    return answer;
}
const analyze = async (ctx) => {
    let result = "<p>服务器出错!请联系管理员</p>";
    await request(ctx.request.query.id)
        .then((res) => {
            let data;
            if (res.status) {
                data = res.body;
                if (data.code === 200) {
                    result = '<p>歌曲id为:' + data.data[0].id + '</p><p>点击<a href="' + data.data[0].url + '">链接</a>下载</p>';
                } else {
                    result = '<p>输入错误!</p>';
                }
            }
        })
        .catch((err) => {
            result = '<p>输入错误!</p><br />' + JSON.stringify(err);
        })
    ctx.response.body = result;
    ctx.response.type = "html";
}
app.use(route.get("/", page))
app.use(route.get("/url", analyze))

app.listen(3000);

4. 实现前端

额,前端我真的不太会,随便写一个吧,能用就行

index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>网易云音乐链接分析器</title>
</head>
<body>
    输入歌曲id:<br>
    <label for="id"></label><input id="id" type="text" name="id" value="">
    <br>
    <button onclick="getWyyyy()">获取下载链接</button>
</body>
<script>
    function getWyyyy() {
        let input = document.getElementById("id").value
        window.location.href = "http://localhost:3000/url?id=" + input
    }
</script>
</html>

记得完成后把localhost换成自己的IP或域名哦(如果像远程使用的话)

测试

运行命令:

node app.js

打开浏览器,输入网址:

http://localhost:3000/

如果一切正常,你将看到你写的前端界面

测试01

输入歌曲id,点击按钮

测试02

测试03

弹出下载界面,成功!


隐私政策

Copyright © Skyone 2025