JoyLau's Blog

JoyLau 的技术学习与思考

今天没有图片

在做单系统的情况下,我还是比较喜欢使用Google 的 Guava 来做缓存的,结合 SpringBoot 使用非常简单 :

1
2
3
4
5
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>

再配置 yml :

1
2
3
4
5
6
spirng:
cache:
type: guava
cache-names: api_cache
guava:
spec: maximumSize=300,expireAfterWrite=2m

上述配置了一个 缓存名为 api_cache 的缓存 ,最大数量为300,超时时间为2分钟

接下来,在类中使用注解 @CacheConfig(cacheNames = “api_cache”) 来配置整个类的配置
@Cacheable() 注解在方法上来 开启方法的注解

使用很透明

今天再次使用时发现guava.spec提示过期了,查了下文档,文档原话是这样说的:

@Deprecated
@DeprecatedConfigurationProperty(
reason = “Caffeine will supersede the Guava support in Spring Boot 2.0”,
replacement = “spring.cache.caffeine.spec”
)

原来,在SpringBoot2.0中推荐使用Caffeine,表达式就是spring.cache.caffeine.spec

更改的方法很简单,改下依赖包,换个配置名,又可以愉快的额使用了:

1
2
3
4
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>

更新配置:

1
2
3
4
5
6
spirng:
cache:
type: caffeine
cache-names: api_cache
caffeine:
spec: maximumSize=300,expireAfterWrite=2m

通常SpringBoot默认的keyGenerator 是SimpleKeyGenerator,这个策略是以参数作为key值,如果参数为空的,就会返回SimpleKey[]字符串,这对于很多无参的方法的就有问题了
我们需要重新这个keyGenerator,实现 org.springframework.cache.interceptor.keyGenerator 这个接口即可,将key值设置为类名+方法名+参数名,这样就不会冲突了

1
2
3
4
5
6
7
8
9
10
11
12
@Bean
public KeyGenerator caffeineKeyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
};
}

感觉无缝切换,继续使用吧!!!

制作背景

  • 有时候宅在家里实在不知道玩什么游戏
  • 英雄联盟都玩烂了
  • 哥们提议玩红警
  • 红警是单机啊,一个人玩另一个人怎么办,一个人打电脑有啥意思 =_=|
  • 找对战平台啊,首先下载安装了红警玩家自制的战网对战平台
  • 我个人电脑从来不安装杀毒软件,Windows Defender 一直报毒搞个不停
  • 战网的平台体验也很不好,消息弹个不停,感觉像广告软件
  • 后来换了腾讯对战平台,进入红警起个名字老说含有敏感信息,结果起了半个小时,MDZZ
  • 决定自己了解下对战平台的原理,打算自己写个简单好用的玩

原理

通过socket hook + udp,针对war3来说,支持tcp,先在本地通过hook模拟建立tcp连接,然后将tcp的数据转成外网udp数据发给外网服务器转发给其他客户端,客户端接收到后通过本地tcp
模拟连接转发到游戏进程。这个过程中通过中转服务器协助进行p2p。
JoyGame-zhihu

上面是知乎上的回答
用我自己的话说就是

使用JoyGameClient客户端,在本地创建了一个虚拟的IP地址,每一个客户端通过连接远程服务器形成了一个虚拟局域网,这样在游戏的【局域网】选择项中就能找到彼此,这样自然一方创建一个游戏,其他人都可以加入进来了就能愉快的玩耍了。底层通信使用的就是TCP和UDP连接,在同一个房间的玩家都会向服务器发送和下载游戏的实时数据。服务器会向房间里的玩家的客户端上转发数据包,这样就间接形成了一个局域网,就能在一起玩游戏啦。

使用

  • 解压,打开JoyGameClient.exe
  • 选择中间的网络服务器,因为你本地肯定是没有服务端的,只能连接远程部署好的服务器
    JoyGame-Login
  • 没有账号,就注册一个账号,注册成功后登录平台
    JoyGame-Login
  • 这是主界面
  • 接下来进入一个你想玩的游戏的房间
  • 设置你的游戏启动主程序
    JoyGame-Login
  • 下面可以设置启动时游戏的参数,比如玩红警时,加入参数 -win,可以窗口启动
  • 之后点启动,进入游戏就可找到在一个房间的小伙伴了
  • 使用都很简单,看一遍就会

特色

  • 可以聊天,发表情,可以加好友。。。额,这些好像没有什么特色
    JoyGame-Login

我想说

如果你想玩玩以前的一些怀旧游戏,或者你想看看该平台是如何操作实现联机的,还等什么,跟着Joy一起来玩吧
私聊我可以给你开个 VIP 、等级直接升到将军哦!虽然没什么用,纯粹装*

下载

一款将 SpringBoot 项目做成Windows Service 的 Maven 插件

包括但不限于 SpringBoot ,任何打成 java jar 包运行的 Maven 项目都可以使用

编写初衷

  • 公司有个项目
  • Java 部分的全部使用的是SpringBoot
  • 该项目的部署环境是 Windows
  • 公司想把 各个 SpringBoot 的模块托管一下
  • 托管的使用方式要简单,易用,测试在打包部署的时候要很容易上手
  • 期间尝试过 Spring Boot Admin 和 Jenkins,都说不好用…
  • 于是就想着 将Spring Boot 的服务制作成 Windows 服务,这样基本上会操作电脑的人都会使用了,够简单易用的了吧
  • 花了一上午时间将其中一个 Spring Boot 模块制作成了 Windows Service
  • 发现再做其他的模块的时候,很多工作都是重复的,心想着能够将这个功能提取出来就好了
  • 于是就写了这个 Maven 插件

使用演示地址:

怎么使用?

  • 使用方法很简单,和普通的 Maven 插件一样使用就可以了,如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <plugins>
    <plugin>
    <groupId>cn.joylau.code</groupId>
    <artifactId>joylau-springboot-daemon-windows</artifactId>
    <version>1.0.RELEASE</version>
    <executions>
    <execution>
    <id>make-win-service</id>
    <phase>package</phase>
    <goals>
    <goal>make-win-service</goal>
    </goals>
    </execution>
    </executions>
    </plugin>
    </plugins>

注意:

  1. 这里的 phase 写的是 package,意思是该插件在 mvn package 的时候调用,你也可以根据不同的需求来更改,比如 install, test等等
  2. goal 写 make-win-service 就可以了,不需要改动
  3. 一般情况下我们的SpringBoot项目会有其他父项目,这时打包会使用 spring-boot-maven-plugin 插件的 repackage,这样的情况的话,请将该插件放置最后面,否则服务运行的话将提示没有主属性
  • 在你的项目中按照以上的方式引入插件后,现在可以 打包了
    1
    mvn package

打包过程中,看到如下日志信息,便制作成功了:
joylau-springboot-daemon-windows-package-info

此时,在你项目的target目录下会生成一个 jar 包名字 一样的压缩包
进入文件夹,解压这个压缩包,你会看见如下内容的文件
joylau-springboot-daemon-windows-package-file
注意:

  1. 5个 bat 文件,请右键以管理员的身份运行
  2. 各文件的文件名无特殊情况,不需要修改
  3. 一旦安装成了 Windows 服务,目录下的文件就不要移动了
  4. 命令运行时,可能会提示安装.NET,安装完成就可运行命令了,不过现在大部分的 Windows 服务器或者个人电脑都会默认安装了.NET,没有的话启用一下就好了,如下图:
    joylau-springboot-daemon-windows-.NET
  5. 运行各个命令是注意提示信息,例如卸载完服务都的状态为NonExistent,刚安装完服务后的状态为Stopped,服务成功启动的状态为Started…等等
    joylau-springboot-daemon-windows-service-status

扩展参数

想要在服务启动时添加自定义参数,如 SpringBoot 的配置参数或者 JMV 参数?
像如下配置即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<plugin>
<groupId>cn.joylau.code</groupId>
<artifactId>joylau-springboot-daemon-windows</artifactId>
<version>1.0.RELEASE</version>
<executions>
<execution>
<id>make-win-service</id>
<phase>package</phase>
<goals>
<goal>make-win-service</goal>
</goals>
</execution>
</executions>
<configuration>
<arguments>
<argument>--server.port=9090</argument>
</arguments>
</configuration>
</plugin>

上面配置了一个 Spring Boot 应用的启动端口9090

使用注意

  • 打包使用过程中需要联网
  • 文档中有些图片可能看不到,再次刷新下页面就可以
  • 服务的id为artifactId,服务的名称为artifactId+version,服务的描述为description

GitHub 地址

源码已开源,地址 : https://github.com/JoyLau/joylau-springboot-daemon-windows

前言

效果展示

JoyMusic-NoReferer
JoyMusic-NoReferer
JoyMusic-NoReferer

在线地址

问题说明

  • 为什么解析的 MV 地址无法直接播放,在上一篇文章上我也说明了
  • 相应的解决办法我在上一篇文章上也说明了
  • 这样的方法有很明显的缺点,在上一篇文章也说明了
  • 这个方法只能实现播放的功能,但是距离完美或者说好的展示效果来说,并不满意
  • 我自己就很不满意

开始动手

先说下我是怎么解决的

  • 解决的方法还是一样:去除referer
  • 同时去除了原来使用的jPlayer播放器,因为这个播放器在移动设备下的表现并不是很好,现在改为浏览器自带的视频播放空控件
  • 这个东西就没有什么兼容性了,只要IE10 以上支持HTML5 的都可以观看
  • 正如上面我截图所示的那样,我使用的是 Safari 浏览器,表现效果还是很好的
  • 同时也加入了一些比较棒的小功能:比如下滑看评论的时候,会出现小视频框在右下角
  • 我个人是比较喜欢看评论的,一些音乐或者 MV 页面打开后并不是先听或者先看,都是翻到下面看看评论
  • 这也正是我喜欢网易云音乐的原因之一,网易云音乐的评论大部分都很精彩,有时候听歌不如看评论

现在是怎么在页面上去除referer的?

  • 动态生成一个iframe,我本身是比较反对使用iframe的,因为以前使用的extjs使用的多了,都用吐了,而且性能还不是很好
  • 但是在这里它可就起了大作用了
  • iframe 里的页面就放一个<video>
  • iframe 的宽度高度及video的宽度高度都要调节好,其实这一步花了我不少时间,因为并不是所有的MV宽高的比例是一样的
  • iframe 的src不能直接写MV的MP4地址,因为那样的话就没有作用了
  • 在src里写js脚本动态生成html页面,页面里面包括的之前提到的video
  • 使用这种方法就可将网站的referer去除掉
  • 这就类似于直接在浏览器的地址栏上输入MP4的地址然后播放
  • 在前一篇的文章分析中,我们知道,这种方法是可以播放的

编写代码

动态渲染iframe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
return '<iframe \
style="border 1px solid #ff0000" \
scrolling="no" \
frameborder="no" \
allowtransparency="true" ' +
/*-- Adding style attribute --*/
objectToHtmlAttributes( iframeAttributes ) +
'id="' + id + '" ' +
' src="javascript:\'\
<!doctype html>\
<html>\
<head>\
<meta http-equiv=\\\'Content-Type\\\'; content=\\\'text/html\\\'; charset=\\\'utf-8\\\'>\
<style>*{margin:0;padding:0;border:0;}</style>\
</head>' +
/*-- Function to adapt iframe's size to content's size --*/
'<script>\
function resizeWindow() {\
var elems = document.getElementsByTagName(\\\'*\\\'),\
width = parent.document.getElementById(\\\'panel-c\\\').offsetWidth-7,\
height = 0,\
first = document.body.firstChild,\
elem;\
if (first.offsetHeight && first.offsetWidth) {\
width = first.offsetWidth;\
height = first.offsetHeight;\
} else {\
for (var i in elems) {\
elem = elems[i];\
if (!elem.offsetWidth) {\
continue;\
}\
width = Math.max(elem.offsetWidth, width);\
height = Math.max(elem.offsetHeight, height);\
}\
}\
var ifr = parent.document.getElementById(\\\'' + id + '\\\');\
ifr.height = height;\
ifr.width = width;\
};\
</script>' +
'<body onload=\\\'resizeWindow()\\\'>\' + decodeURIComponent(\'' +
/*-- Content --*/
encodeURIComponent(html) +
'\') +\'</body></html>\'"></iframe>';

注意这里的反斜杠不要去掉,是用来转义的,代码的样式虽然丑了点,但是并不影响使用

  • 这里面有个方法是encodeURIComponent(html),这个是转义了video里面的url链接
  • 在iframe的body加载完成后会调用resizeWindow()函数自适应下iframe的宽高
  • html里面写的就是要放入iframe的body里的代码,这里我们放的肯定是video
  • 于是,可以将上述代码封装成一个函数,在父页面是直接调用
  • 封装的时候我们还可以传一些参数,比如上面的iframe的初始的宽高,style,scrolling,frameborder等等

扩展一下

  • 这个方式使用的是video
  • 那么<img>呢?现在有些网站的图片也是经过了防盗链处理,这种方法也是可以实现去掉referer,直接访问图片的额

欢迎大家来看看试试看!😘 http://music.joylau.cn (当前版本 v1.5)

前言

效果展示

在线地址

开始

需要准备

  • 这次要解析的是 网易云音乐的 MV
  • 需要准备的解析的有
  • 获取 MV 信息列表
  • 获取 MV 详细信息
  • 获取 MV 播放地址
  • 在线播放 MV
  • 获取 MV 排行榜
  • 获取最新 MV

说明

  • 大部分解析提供的接口都和我以前2篇文章类似,之前的文章有分析过,这里就不再多说了
  • 这里重点说明下 MV 的播放问题

关于 MV 的播放

  • 解析 MV 详细信息,可获得 MV 的真实播放的 MP4 的地址
  • 但是这个地址,网易云做了防盗链处理
  • 什么是防盗链?
  • 一般情况下,我的资源文件,比如 图片, css,js,视频,我们自己放到服务器上可以直接引用
  • 同样的道理,别人可以访问你的服务器,也可以直接引用
  • 那么,不想被别人引用怎么办呢?
  • 这就引申出了防盗链的操作
  • 最常见的防盗链的处理就是加上 referer识别,就是来源网址信息
  • referer 其实是个错误的拼写,这个就是有历史原因了,以前的开发人员在定义这个属性的时候,把这个单词写错了,后来没有人注意到,一直使用到他作为标准
  • 后来,也没有人去特意改他了,就这么用着吧
  • 这个是简单防盗链处理
  • 还有更复杂的,比如 js 加密路径信息,每次请求路径都会变化,这个就复杂了
  • 很幸运,网易云的 MV 采用的就是 referer 的识别方式
  • 那么就有相应的破解方法了

关于 referer

  • MP4 的地址在浏览器地址栏直接粘过去是可以播放的,但是由其他网站跳进去的则不能访问,因为带进了 rerferer
  • 那么,要做的就是去除 请求的 rerferer
  • 我找了很多资料也尝试了很多次,想在浏览器端把 rerferer 去除掉,基本是实现不了的,如果你实现在页面里单独请求 mp4 地址时不带referer, 请联系我
  • 那么要做的就是在服务端操作了
  • 在服务端操作很简单,就是伪造头信息进行请求

这个是带 referer 的请求,被网易云直接拒绝了

joymusic-mv-referer

这个是复制地址到地址栏,则可以直接播放

joymusic-mv-no-referer

服务单去除 referer

  • 严格来说不能说去除 refere,我们需要将原本我们自己服务器的 referer 修改为网易云服务器的 referer

Java 版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void playMV(HttpServletResponse res, String mvurl) throws IOException {
if (StringUtils.isEmpty(mvurl)){
return;
}
res.setContentType("video/mpeg4; charset=utf-8");
URLConnection connection = new URL(mvurl).openConnection();
connection.setRequestProperty("referer", "http://music.163.com/");
connection.setRequestProperty("cookie", "appver=1.5.0.75771;");
connection.connect();
InputStream is = connection.getInputStream();
OutputStream os = res.getOutputStream();
byte bf[] = new byte[2048];
int length;
try {
while ((length = is.read(bf)) > 0) {
os.write(bf, 0, length);
}
} catch (IOException e) {
is.close();
os.close();
return;
}
is.close();
os.close();
}

解释:

  1. 首先我们请求的资源不是本地的资源,是存储在其他服务器上的,这里用到的是URL
  2. 这里我们需要设置 referer 和 cookie,结合前面使用的 URL, 这里使用的是URLConnection
  3. 后面的就很好理解了,相当于做了一个管道,将读取的文件流原封不动的通过Response返回给调用者
  4. 不要忘了设置 setContentType 为 MP4 的格式

nodejs 版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const express = require("express");
const router = express();
const request = require("request");

router.get("/", (req, res) => {
const url = req.query.url;
const headers = {
"Referer": "http://music.163.com/",
"Cookie": "appver=1.5.0.75771;",
'Content-Type': 'video/mp4',
'Location': url
};
const options = {
header: headers,
url: url
};
request(options).on('error', err => {
res.send({ err })
}).pipe(res)
});

module.exports = router;

解释:
和上面的 Java 版代码是一个意思,主要是 pipe 流管道将文件流返回给调用者

功能完成

  • 那么这样解决了 MP4 地址防盗链的问题

缺点

  • 不足之处也暴露了
  • 首先这段代码必须部署到服务端
  • 部署到服务端就需要服务器去拉去 MV 的流信息,这无疑给服务器增加过多的流量压力
  • 其次,由于使用的流传输,这个 MP4 的播放是不支持快进操作的

有个简单的解决方式

  • 在 html5 之后,想去除 referer 信息, a标签有个属性 rel
  • rel="noreferrer" 即可在 a 标签的 href 的链接上去除 referer信息
  • 这一属性已被我使用在播放器的右下角的一个小飞机的按钮上
  • 点击小飞机按钮就可以直接看 MV 视频了,流量走的是网易云的CDN,不再试自己的服务器

joymusic-mv-no-referer-href

不完美

  • 总感觉这个解决不够完美
  • 如果你看到这篇文章能有更好的解决办法,请联系我

欢迎大家来看看试试看!😘 http://music.joylau.cn (当前版本 v1.4)

前言

效果展示

JoyMedia - Search

在线地址

解释

  • 正如文章图片那样,在搜索框中输入想听的音乐/歌手/专辑
  • 在输入过程中及输入完成后,显示搜索结果的列表供用户选择

材料

  • REST 接口
  • jquery-autocomplete插件

优美的开始

准备工作

  • 引入插件 css: jquery.autocomplete.css
  • 引入插件 js : jquery.autocomplete.min.js
  • 写一个数据返回的 REST 接口

开始操作

  • 定义搜索的 input 的 id 值
1
2
3
4
5
6
7
8
9
10
<div class="navbar-form navbar-left input-s-lg m-t m-l-n-xs hidden-xs">
<div class="form-group" style="display: inline">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-sm bg-white btn-icon rounded"><i class="fa fa-search"></i></button>
</span>
<input id="keywords" type="text" class="form-control input-sm no-border rounded" placeholder="搜索 单曲/歌手/专辑...">
</div>
</div>
</div>
  • 这里我定义的是 keywords
  • 接下来在我们的 js 文件里调用 : $(“#keywords”).autocomplete
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$("#keywords").autocomplete("/music/neteaseCloud/search", {
width : 350, // 提示的宽度,溢出隐藏
max : 30,// 显示数量
scrollHeight: 600,
resultsClass: "ac_results animated fadeInUpBig",
autoFill : false,//自动填充
highlight : false,
highlightItem: true,
scroll : true,
matchContains : true,
multiple :false,
matchSubset: false,
dataType: "json",
formatItem: function(row, i, max) {
//自定义样式
},
formatMatch: function(row, i, max) {
return row.name + row.id;
},
formatResult: function(row) {
return row.id;
},
parse:function(data) {
//解释返回的数据,把其存在数组里
if (data.data.length === 0) {
return [];
}else {
return $.map(data.data, function(row) {
return {
data: row
}
});
}

}
}).result(function(event, row, formatted) {
jQuery(this).val(row.name + ' ' + row.author);
addSearchResult(row.id);
});

接下来重点解释这个配置项

  • autocomplete 的第一个参数是url, 值得注意的是,这个 url 我们返回的结果数据是 JSON
  • 后面要专门针对返回的 JSON 数据进行解析
  • 再往后面来,看到的是一些配置项参数,一些简单的我就不在这多解释了,我这边主要说下我觉得比较重要的
  • resultsClass : 这个参数是生成的候选项的父 DIV,如下图所示:

JoyMedia - AutoComplate-Div

  • 默认提供的样式很不好看,默认提供的样式都写在 jquery.autocomplete.css 里面
  • 在这里面,能看到刚才截图的 div : ac_results
  • 那么我们要美化的就是 这个 div 和其子元素 li 的样式了
  • 为了跟契合本站的主题,我采用的黑色主题风格
  • 给ac_results添加了黑色背景色:background-color: #232c32
  • 在js文件里搜索ac_results,添加动画效果,并将这个配置写到配置项里:resultsClass: “ac_results animated fadeInUpBig”
  • ul 里的 li 是交替的样式的,class 分别为ac_odd和 ac_even,鼠标滑上去的效果为 ac_over,这几个地方自定义下样式
  • 还有一个配置: matchSubset,设置为 false ,可以避免输入大小写转换的js错误
  • formatItem : 返回的每一个结果都会再次处理,这里要做的事是以自己想要的样式显示出来
  • formatMatch : 匹配自己在结果集中想要的属性
  • formatResult : 自己最终要取的数据是什么
  • parse : 针对返回的JSON 数据进行转换,这里通过$. map 转化为 数组
  • result : 点击了列表项以后要做什么事情

完美的结束

欢迎大家来听听试试看!😘 http://music.joylau.cn (当前版本 v1.3)

前言

在线地址

Node.js 的学习

  • 入门是从这本书上开始的
  • 结合Node中文网的文档开始探索开发

说明

  • 利用 Node 来解析网易云音乐,其实质就是 跨站请求伪造 (CSRF),通过自己在本地代码中伪造网易云的请求头,来调用网易云的接口

分析

以获取歌曲评论来分析

  • 我们打开其中一首音乐,抓包看一下

JoyMedia - Node

  • 绝大部分的请求都是 POST 的
  • 我们找到其中关于评论的请求,如上图所示
  • 链接中间的部分是歌曲的 id 值
  • 在返回的 JSON 数据中包含了热评和最新评论
  • 评论过多的话是分页来展示的
  • 通过参数 limit 来显示评论数量, offset 来控制分页

JoyMedia - Node

  • 再来看,这是我本地浏览器中的 cookies 值,现在为止知道有个 csrf 值用来加密

JoyMedia - Node

  • 每个请求后面都会跟上csrf_token 值,其他的参数还有params 和 encSecKey
  • 这些值的加密算法无非是2种,一种是前台 js 加密生成的,另一种是将参数传往后台,由后台加密完再传回来
  • 想要测试一下很简单,将里面的值复制一下在 xhr 里找一下就知道了
  • 推测是是 js 加密的,加密的 js 简直不能看,如下图

JoyMedia - Node

  • 看到很多请求后面都返回了 md5 那么 md5 加密是肯定有的
  • 其实仔细看加密的参数,很多都能靠猜出来
  • 本地需要创建一个私钥secKey,十六位,之后aes加密生成,在通过rsa吧secKey加密作为参数一起传回
  • 那么下面贴出加密代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7';
const nonce = '0CoJUm6Qyw8W8jud';
const pubKey = '010001';
function createSecretKey(size) {
const keys = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let key = "";
for (let i = 0; i < size; i++) {
let pos = Math.random() * keys.length;
pos = Math.floor(pos);
key = key + keys.charAt(pos)
}
return key
}

function aesEncrypt(text, secKey) {
const _text = text;
const lv = new Buffer('0102030405060708', "binary");
const _secKey = new Buffer(secKey, "binary");
const cipher = crypto.createCipheriv('AES-128-CBC', _secKey, lv);
let encrypted = cipher.update(_text, 'utf8', 'base64');
encrypted += cipher.final('base64');
return encrypted
}

function zfill(str, size) {
while (str.length < size) str = "0" + str;
return str
}

function rsaEncrypt(text, pubKey, modulus) {
const _text = text.split('').reverse().join('');
const 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)
}

function Encrypt(obj) {
const text = JSON.stringify(obj);
const secKey = createSecretKey(16);
const encText = aesEncrypt(aesEncrypt(text, nonce), secKey);
const encSecKey = rsaEncrypt(secKey, pubKey, modulus);
return {
params: encText,
encSecKey: encSecKey
}
}
  • 挺复杂的,很多我也是参考网络上其他人的加密方式

伪造网易云头部请求

  • 这一步就很简单了,主要需要注意的就是 referer 的地址一定要是网易云的地址
  • 其他的想 cookie 和 User-Agent 直接复制浏览器的即可
  • 那我们构造一个 POST 的请求
  • 需要都回到函数和错误返回回调函数
  • 贴下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const Encrypt = require('./crypto.js');
const http = require('http');
function createWebAPIRequest(host, path, method, data, cookie, callback, errorcallback) {
let music_req = '';
const cryptoreq = Encrypt(data);
const http_client = http.request({
hostname: host,
method: method,
path: path,
headers: {
'Accept': '*/*',
'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': 'http://music.163.com',
'Host': 'music.163.com',
'Cookie': cookie,
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36',

},
}, function (res) {
res.on('error', function (err) {
errorcallback(err)
});
res.setEncoding('utf8');
if (res.statusCode !== 200) {
createWebAPIRequest(host, path, method, data, cookie, callback);

} else {
res.on('data', function (chunk) {
music_req += chunk
});
res.on('end', function () {
if (music_req === '') {
createWebAPIRequest(host, path, method, data, cookie, callback);
return
}
if (res.headers['set-cookie']) {
callback(music_req, res.headers['set-cookie'])
} else {
callback(music_req)
}
})
}
});
http_client.write('params=' + cryptoreq.params + '&encSecKey=' + cryptoreq.encSecKey);
http_client.end()
}
  • 那么再结合我们刚才分析的评论API, 发出该请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const express = require("express");
const router = express();
const { createWebAPIRequest } = require("../common");

router.get("/", (req, res) => {
const rid=req.query.id;
const cookie = req.get('Cookie') ? req.get('Cookie') : '';
const data = {
"offset": req.query.offset || 0,
"rid": rid,
"limit": req.query.limit || 20,
"csrf_token": ""
};
createWebAPIRequest(
'music.163.com',
`/weapi/v1/resource/comments/R_SO_4_${rid}/?csrf_token=`,
'POST',
data,
cookie,
music_req => {
res.send(music_req)
},
err => res.status(502).send('fetch error')
)
});

module.exports = router;
  • 值得注意的是,这里我的 node 模板选择的 EJS 所使用的 js 语法格式也比较新,你需要将你 WebStorm 的 js 编译器的版本提升到ECMAScript 6,否则的话会报错,如下图所示:
    JoyMedia - Node

封装

  • 我们写一个入口文件,可以直接运行期容器,以及提供 APIs
  • 那么,这个就跟简单了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const express = require('express');
const http = require('http');
const app = express();


const port = 3000;

const v = '/apis/v1';

app.listen(port, () => {
console.log(`server starting ${port}`)
});

/*APIs 列表*/
app.use(express.static('public'));


//推荐歌单
app.use(v + "/personalized", require("./apis/personalized"));

//歌单评论
app.use(v + '/comment/playlist', require('./apis/comment_playlist'));

//获取歌单内列表
app.use(v + '/playlist/detail', require('./apis/playlist_detail'));

//获取音乐详情
app.use(v + '/song/detail', require('./apis/song_detail'));

//单曲评论
app.use(v + '/comment/music', require('./apis/comment_music'));

//获取音乐 url
app.use(v + '/music/url', require('./apis/musicUrl'));

// 获取歌词
app.use(v + '/lyric', require('./apis/lyric'))


process.on('uncaughtException', function (err) {
//打印出错误的调用栈方便调试
console.log(err.stack);
});


module.exports = app;
  • 引用 http 模块,开启 node 的默认3000 端口
  • 目前提供了上述注释里所写的 APIs
  • 每一个 API 都会单独写一个模块,以在此调用
  • 有一个地方值得注意的事
  • node 是单线程的异步 IO,这使得他在高并发方面得到很快相应速度,但是也有缺点
  • 当其中一个操作出错异常了,就会导致整个服务挂掉
  • 我在此的处理方式是:监听全局异常,捕到异常后将错误的堆栈信息打印出来,这样使得后续的操作不得进行以至于使整个服务挂掉
  • 当然,还有其他的方式来处理,可以通过引用相应的模块,来守护 node 的进程,简单的来说就是挂掉我就给你重启
  • 我觉得第二种方式不是我想要的,我是采取的第一种方式
  • 况且我还真想看看是什么错误引起的
  • 最后发现都是网络原因引起的错误 🤣🤣🤣🤣😂😂😂😂😂

运行

  • npm install
  • node app.js

查看效果

JoyMedia - Node

JoyMedia - Node

欢迎大家来听听试试看!😘 http://music.joylau.cn (当前版本 v1.3)

JoyMedia - Beta - 预览图
JoyMedia - Beta - 系统结构

在线地址

JoyMedia - Beta 预览版

项目介绍

实现目的

  • 本人经常在写代码或者没事的时候会听一些音乐
  • 以前大部分会选择本地安装客户端
  • 其中最喜欢的认为做的比较好的音乐客户端实属网易云音乐了
  • 无论是从 Mac 版的客户端,还是 IOS 版的客户端,界面都非常优美,简直是极客和码农的必备
  • 最主要是的网易云的歌曲推荐功能,很强大,我一度认为2个人的歌单相似度超过90%,就可以在一起了,这样再也不怕找不到对象了,😆
  • 但也有些问题,网易云有一些版权音乐,是无法听到的,有时候昨天还在听得音乐,今天就听不了了
  • 这就很烦了

自己的想法

  • 最初想把这个版权音乐的 mp3 地址解析出来,这样就可以直接听了
  • 恩,想法很 nice
  • 那么,照着这个想法做吧

项目实现

总体架构

  • 正如上述系统结构所示
  • 我自己有2台云服务器,一台阿里云的,另一台是腾讯云的
  • 这2台服务器,我是这样分配的: 阿里云只提供 WEB 服务,腾讯云为 WEB 访问提供各种服务
  • 当然服务器上我还跑了其他服务

阿里云服务器

  • Nginx 主要负责了 JoyMedia 的 负载均衡,在该台服务器上,我用 部署了2个 spring-boot 项目,以权重的方式配置了负载均衡,这样我在更新项目的时候可以保证另一个服务的可用性
  • 当然 Nginx 还有个反向代理的作用, upstream 配置了其他项目的访问
  • 还有台 Redis 服务了,爬到的数据会存到 Redis 了,以供 WEB 服务迅速读取,当然在有些地方不会读取 Redis ,比如单曲歌曲播放的 mp3地址的获取
  • 在最开始的时候我会先在后台解析出来再存到 Redis 里,但是发现网易云的歌曲 mp3 地址失效太快了,有时会导致播放异常,不如实时解析来的实在
  • 在比如单首歌曲的评论的获取,这个得是实时解析的

腾讯云服务器

  • 提供网易云音乐解析的是一个 Node 服务,这个 Node 服务是如何解析地址的,这个需要单独再写一篇文章,先知道这个 Node 服务是干嘛的就好
  • 然后部署了3个spring-boot服务,分别提供了各自的服务,有定时爬去网易云音乐的推荐歌单,爬取歌单的歌曲列表,爬取歌单评论
  • 由于爬到的音乐信息很快就会失效,这个服务都要定时的爬取
  • 爬取到的数据的落地存储,我是存到的MongoDB中,在这篇文章中:重剑无锋,大巧不工 SpringBoot — 整合使用MongoDB , 我说明了为什么要选择 MongoDB
  • 这3个服务爬到的数据会实时存到 Redis 中,另一方面,会异步存到 MongoDB 中,我想着这些数据或许还能做什么数据分析之类的,😄

初版完成后

等我搭建完这个服务后,发现了问题

  • 有版权控制的音乐根本解析不到 mp3 的实际地址
  • 那么我想听的音乐,听不到还是听不到,突然变得很尴尬

又有了想法

  • 一般情况下,我们在一家音乐网站上找不到自己想要的音乐,就回去其他音乐网站上找
  • 恩,就这么干
  • 网易云找不到的音乐,我就去虾米音乐,去 QQ 音乐找
  • 这2个网站的音乐我都小试了下,都是可以的
  • 于是我现在把这些功能集中在页面的搜索框中,搜索这3个音乐网站的结果,然后实施解析来播放
  • 这是我下步要做的事情

有些地方还有 BUG

  • 有些地方还是有 BUG 的,需要修复

有些地方功能还没写好

  • 比如右上角的用户登录,现在的想法是使用第三方登录,比如 QQ, 微信…,但是是登录网易云音乐呢,还是登录网站呢?
  • 要是登录网易云音乐的话,估计账号安全是个问题,而且登录接口不能频繁调用
  • 要是登录网站的,好像没什么卵用
  • 再比如左下角的歌词界面,虽然能获取到歌词,但是怎么做到歌词随着歌曲的播放实时滚动,这个现在还没有头绪…

还在继续开发中…

0%