十次方项目-day03
第3章 - 即时通讯和接口加密
1 即时通讯的业务场景和需求
即时通信(Instant Messaging,简称IM)是一个允许两人或多人使用网络实时的传递文字消息、文件、语音与视频交流。 即时通讯技术应用于需要实时收发消息的业务场景。
现在各种各样的即时通讯软件也层出不穷:
- 客服系统
- 直播互动
- 社交APP
- 智能硬件、物联网
2 短连接和长连接
即时通讯使用的是长连接,这里我们介绍一下短连接和长连接。
2.1 短连接
客户端和服务器每进行一次通讯,就建立一次连接,通讯结束就中断连接。

HTTP是一个简单的请求-响应协议,它通常运行在TCP之上。HTTP/1.0使用的TCP默认是短连接。
2.2 长连接
是指在建立连接后可以连续多次发送数据,直到双方断开连接。

HTTP从1.1版本起,底层的TCP使用的长连接。
使用长连接的HTTP协议,会在响应头加入代码:Connection:keep-alive
2.3 短连接和长连接的区别
2.3.1 通讯流程
短连接:创建连接 -> 传输数据 -> 关闭连接
长连接:创建连接 -> 传输数据 -> 保持连接 -> 传输数据 -> …… -> 关闭连接
2.3.2 适用场景
短连接:并发量大,数据交互不频繁情况
长连接:数据交互频繁,点对点的通讯
2.3.3 通讯方式
| 方式 | 说明 |
|---|---|
| 短连接 | 我跟你发信息,必须等到你回复我或者等了一会等不下去了(连接超时),就结束通讯了 |
| 长连接 | 我跟你发信息,一直保持通讯,在保持通讯这个时段,我去做其他事情的当中你回复我了,我能立刻你回复了我什么,然后可以回应或者不回应,继续做事 |
3 websocket协议
3.1 何为websocket协议
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
- 何谓全双工:全双工(Full Duplex)是通讯传输的一个术语。双方在通信时允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合。全双工指可以同时进行信号的双向传输。指A→B的同时B→A,就像是双向车道。
- 单工就就像是汽车的单行道,是在只允许甲方向乙方传送信息,而乙方不能向甲方传送 。
参考资料:https://baike.baidu.com/item/%E5%85%A8%E5%8F%8C%E5%B7%A5/310007?fr=aladdin
在 WebSocket中,浏览器和服务器只需要完成一次握手,就可以创建持久性的连接,并进行双向数据传输。
在推送功能的实现技术上,相比使用Ajax 定时轮询的方式(setInterval),WebSocket 更节省服务器资源和带宽。
服务器向客户端发送数据的功能是websocket协议的典型使用场景

3.2 websocket常用事件方法
以下 API 用于创建 WebSocket 对象。
1 | var Socket = new WebSocket(url, [protocol] ); |
WebSocket 事件
以下是 WebSocket 对象的相关事件。假定我们使用了以上代码创建了 Socket 对象:
| 事件 | 事件处理程序 | 描述 |
|---|---|---|
| open | Socket.onopen | 连接建立时触发 |
| message | Socket.onmessage | 客户端接收服务端数据时触发 |
| error | Socket.onerror | 通信发生错误时触发 |
| close | Socket.onclose | 连接关闭时触发 |
WebSocket 方法
| 方法 | 描述 |
|---|---|
| Socket.send() | 使用连接发送数据 |
| Socket.close() | 关闭连接 |
4 十次方的im功能
4.1 系统设计
4.1.1 技术选型
- 环信im云
- 前端框架 vue
4.1.2 架构设计
前端页面使用十次方用户微服务认证用户身份,使用环信im云进行即时消息通信。

4.2 环境和工具
- nodejs
- npm
- 前端框架 vue
- 开发工具 vscode
4.3 环信im云介绍
环信im云是即时通讯云 PaaS 平台,开发者可以通过简单的SDK和REST API对接。
- 支持安卓,iOS,Web等客户端SDK对接
- 提供单聊,群聊,聊天室等即时通讯功能
- 支持富媒体消息,实时音视频和各种自定义的扩展消息
4.3.1 注册账号
网址:https://console.easemob.com/user/register
4.3.2 创建应用
登陆环信im云,按照下图操作
进入刚才的应用,获取appkey,orgname,client id,client secret等字段

4.3.3 接口测试-获取token
- 使用环信提供的swagger接口调试页面测试接口
页面网址:http://api-docs.easemob.com/#/%E8%8E%B7%E5%8F%96token
- 使用postman测试接口
4.3.4 im系统架构

4.4 十次方即时通讯功能
4.4.1 用户微服务实现
创建tensquare_user子模块
创建Maven工程
在pom.xml中添加依赖
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<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>com.tensquare</groupId>
<artifactId>tensquare_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- mybatis-plus begin -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatisplus-spring-boot-starter</artifactId>
<version>${mybatisplus-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<!-- mybatis-plus end -->
</dependencies>编写application.yml配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25server:
port: 9008
spring:
application:
name: tensquare-user
datasource: # 数据库连接四大属性
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.200.128:3306/tensquare_user?characterEncoding=utf-8
username: root
password: root
# Mybatis-Plus 配置
mybatis-plus:
# mapper-locations: classpath:/mapper/*Mapper.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.tensquare.article.pojo
global-config:
id-type: 1 #0:数据库ID自增 1:用户输入id
db-column-underline: false
refresh-mapper: true
configuration:
map-underscore-to-camel-case: true
cache-enabled: true #配置的缓存的全局开关
lazyLoadingEnabled: true #延时加载的开关
multipleResultSetsEnabled: true #开启延时加载,否则按需加载属性
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql语句,调试用编写MyBatis配置Bean
1
2
3
4
5
6
7
8
9
public class MybatisPlusConfig {
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}编写引导类
1
2
3
4
5
6
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}编写pojo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class User implements Serializable {
private String id;
private String mobile;
private String password;
private String nickname;
private String sex;
private Date birthday;
private String avatar;
private String email;
private Date regdate;
private Date updatedate;
private Date lastdate;
private Long online;
private String interest;
private String personality;
private Integer fanscount;
private Integer followcount;
//get set...
}编写dao
1
2public interface UserDao extends BaseMapper<User> {
}编写service
1
2
3
4
5
6
7
8
9
10
public class UserService {
private UserDao userDao;
public User login(User user) {
return userDao.selectOne(user);
}
}编写controller
1
2
3
4
5
6
7
8
9
10
11
12
class UserController {
private UserService userService;
public Result login( User user) {
User result = userService.login(user);
if (result != null) {
return new Result(true, StatusCode.OK, "登录成功", result);
} return new Result(false, StatusCode.OK, "登录失败");
}
}
4.4.2 即时通讯前端准备
访问环信IM开发文档—> Web客户端 —> SDK集成介绍 —》Web IM 集成介绍
或者直接访问http://docs-im.easemob.com/im/web/intro/integration
按照文档,使用git下载集成案例:
1
$ git clone https://github.com/easemob/webim.git
复制案例中的\webim\sdk目录下的所有js文件到项目resources\static\js中
复制webim\simpleDemo中的资料到resources\static中
效果如下:

测试demo.html,确认即时通讯的用户登录,发文本消息,效果如下:

4.4.3 发送和接收消息
复制Spring-websocket项目中的chatroom.jsp改造为chatroom.html,根据demo.html案例实现用户注册和登录和即时消息功能。最终效果:
1 |
|
5 接口加密
5.1 业务场景介绍
数据安全性 - 抓包工具
wireshark
fiddler
charles
系统明文传输的数据会被不明身份的人用抓包工具抓取,从而威胁系统和数据的安全性
5.2 加密方式
5.2.1 摘要算法
消息摘要是把任意长度的输入揉和而产生长度固定的信息。
消息摘要算法的主要特征是加密过程不需要密钥,并且经过加密的数据无法被解密,只有输入相同的明文数据经过相同的消息摘要算法才能得到相同的密文。消息摘要算法不存在密钥的管理与分发问题,适合于分布式网络上使用。
消息摘要的主要特点有:
- 无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。
- 消息摘要看起来是“随机的”。这些数据看上去是胡乱的杂凑在一起的。
- 只要输入的消息不同,对其进行摘要后产生的摘要消息也必不相同;但相同的输入必会产生相同的输出。
- 只能进行正向的消息摘要,而无法从摘要中恢复出任何消息,甚至根本就找不到任何与原信息相关的信息。
- 虽然“碰撞”是肯定存在的,但好的摘要算法很难能从中找到“碰撞”。即无法找到两条不同消息,但是它们的摘要相同。
常见的摘要算法:CRC、MD5、SHA等
5.2.2 对称加密
对称加密的特点:
- 速度快,通常在消息发送方需要加密大量数据时使用。
- 密钥是控制加密及解密过程的指令。
- 算法是一组规则,规定如何进行加密和解密。
典型应用场景:离线的大量数据加密(用于存储的)
常用的加密算法:DES、3DES、AES、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK等。
对称加密的工作过程如下图所示

加密的安全性不仅取决于加密算法本身,密钥管理的安全性更是重要。如何把密钥安全地传递到解密者手上就成了必须要解决的问题。
5.2.3 非对称加密
非对称加密算法是一种密钥的保密方法,加密和解密使用两个不同的密钥,公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。
非对称加密算法的特点:
- 算法强度复杂
- 加密解密速度没有对称密钥算法的速度快
经典应用场景:数字签名(私钥加密,公钥验证)
常用的算法:RSA、Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)。
非对称加密算法示意图如下

5.2.4 数字签名
数字签名(又称公钥数字签名)是一种类似写在纸上的普通的物理签名,是使用了公钥加密领域的技术实现,用于鉴别数字信息的方法。
数字签名通常使用私钥生成签名,使用公钥验证签名。
签名及验证过程:
- 发送方用一个哈希函数(例如MD5)从报文文本中生成报文摘要,然后用自己的私钥对这个摘要进行加密
- 将加密后的摘要作为报文的数字签名和报文一起发送给接收方
- 接收方用与发送方一样的哈希函数从接收到的原始报文中计算出报文摘要,
- 接收方再用发送方的公用密钥来对报文附加的数字签名进行解密
- 如果这两个摘要相同、接收方就能确认该数字签名是发送方的。

数字签名验证的两个作用:
- 确定消息确实是由发送方签名并发出来的
- 确定消息的完整性
5.3 OpenSSL生成rsa密钥对
5.3.1 RSA算法的密钥格式
密钥长度介于 512 - 65536 之间(JDK 中默认长度是1024),且必须是64 的倍数。密钥的常用文件格式有pem(文本存储)或者der(二进制存储)。
当使用Java API生成RSA密钥对时,公钥以X.509格式编码,私钥以PKCS#8格式编码
RSA使用pkcs协议定义密钥的存储结构等内容
| 协议 | 说明 |
|---|---|
| PKCS#1 | 定义了RSA公钥函数的基本格式标准,特别是数字签名。 |
| PKCS#2 | 涉及了RSA的消息摘要加密,已被并入PKCS#1中。 |
| PKCS#3 | Diffie-Hellman密钥协议标准。 |
| PKCS#4 | 最初是规定RSA密钥语法的,现已经被包含进PKCS#1中。 |
| PKCS#5 | 基于口令的加密标准,描述了使用由口令生成的密钥来加密8位位组串并产生一个加密的8位位组串的方法。PKCS#5可以用于加密私钥,以便于密钥的安全传输(这在PKCS#8中描述)。 |
| PKCS#6 | 扩展证书语法标准,定义了提供附加实体信息的X.509证书属性扩展的语法。 |
| PKCS#7 | 密码消息语法标准。为使用密码算法的数据规定了通用语法,比如数字签名和数字信封。 |
| PKCS#8 | 私钥信息语法标准。定义了私钥信息语法和加密私钥语法,其中私钥加密使用了PKCS#5标准。 |
| PKCS#9 | 可选属性类型。 |
| PKCS#10 | 证书请求语法标准。 |
| PKCS#11 | 密码令牌接口标准。 |
| PKCS#12 | 个人信息交换语法标准。 |
| PKCS#13 | 椭圆曲线密码标准。 |
| PKCS#14 | 伪随机数产生标准。 |
| PKCS#15 | 密码令牌信息语法标准。 |
- pkcs标准详细说明:https://www.rfc-editor.org/search/rfc_search_detail.php?title=pkcs&pubstatus%5B%5D=Any&pub_date_type=any
- RSA官方网站:https://www.rsa.com
5.3.2 openssl生成rsa密钥对的命令
1.生成rsa私钥,文本存储格式,长度2048
1 | openssl genrsa -out ../mycerts/rsa_private_key.pem 2048 |

2.根据私钥生成对应的公钥
1 | openssl rsa -in ../mycerts/rsa_private_key.pem -pubout -out ../mycerts/rsa_public_key_2048.pub |

3.私钥转化成pkcs8格式
1 | openssl pkcs8 -topk8 -inform PEM -in ../mycerts/rsa_private_key.pem -outform PEM -nocrypt > ../mycerts/rsa_private_key_pkcs8.pem |

然后再mycerts下查看密钥

5.4 搭建接口加密微服务
接口加解密请求参数的流程

5.4.1 修改tensquare_parent
在十次方parent父工程pom.xml中添加SpringCloud依赖
1 | <dependencyManagement> |
5.4.2 创建Eureka微服务
创建Maven工程tensquare_eureka,在pom.xml中添加以下依赖:
1 | <dependencies> |
添加配置文件:
1 | server: |
编写启动类
1 |
|
启动服务,并访问端口6868:

5.4.3 修改文章微服务
在pom.xml中添加Eureka依赖
1 | <dependency> |
修改配置文件,使用Eureka
1 | eureka: |
在ArticleApplication添加@EnableEurekaClient依赖
1 | import org.mybatis.spring.annotation.MapperScan; |
5.4.3 创建tensquare_encrypt网关服务
在tensquare_parent父工程下新建tensquare_encrypt子模块,并按下面的步骤添加配置和代码
在pom.xml文件中添加以下配置
1
2
3
4
5
6
7
8
9
10
11
12<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
</dependencies>在resource文件夹下新建application.yml文件,并添加如下配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19server:
port: 9013
spring:
application:
name: tensquare-encrypt
zuul:
routes:
tensquare-article: #文章
path: /article/** #配置请求URL的请求规则
serviceId: tensquare-article #指定Eureka注册中心的服务id
strip-prefix: true #所有的article请求都进行转发
sentiviteHeaders:
customSensitiveHeaders: true #让zuul网关处理cookie和重定向
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka/
instance:
prefer-ip-address: true新建com.tensquare.encrypt包,并在包下新建启动类EncryptApplication,添加如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
//开启zuul网关
//防止启动url报错
public class EncryptApplication {
public static void main(String[] args) {
SpringApplication.run(EncryptApplication.class,args);
}
}将rsa相关的工具类复制到在com.tensquare.encrypt包下,工具类的位置在 资料\工具类\RSA 文件夹下,分别为rsa和service文件夹,均复制到com.tensquare.encrypt包下。
注意将公钥和私钥复制粘贴为自己的
在src/test/java文件夹下创建测试用例EncryptTest,该测试用例用于将请求参数加密,代码如下
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
41import com.tensquare.encrypt.EncryptApplication;
import com.tensquare.encrypt.rsa.RsaKeys;
import com.tensquare.encrypt.service.RsaService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
public class EncryptTest {
private RsaService rsaService;
public void before() throws Exception {
}
public void after() throws Exception {
}
public void genEncryptDataByPubKey() {
//此处可替换为你自己的请求参数json字符串
String data = "{\"title\":\"java\"}";
try {
String encData = rsaService.RSAEncryptDataPEM(data, RsaKeys.getServerPubKey());
System.out.println("data: " + data);
System.out.println("encData: " + encData);
} catch (Exception e) {
e.printStackTrace();
}
}
}6.编写filter
在com.tensquare.encrypt包下新建filters包,然后新建过滤器类RSARequestFilter,添加下面的代码
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109package com.tensquare.encrypt.filters;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.netflix.zuul.http.ServletInputStreamWrapper;
import com.tensquare.encrypt.rsa.RsaKeys;
import com.tensquare.encrypt.service.RsaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class RSARequestFilter extends ZuulFilter {
private RsaService rsaService;
public String filterType() {
//过滤器在 什么环境执行,解密操作需要在转发之前执行
return FilterConstants.PRE_TYPE;
}
public int filterOrder() {
//设置过滤器的执行顺序
return FilterConstants.PRE_DECORATION_FILTER_ORDER;
}
public boolean shouldFilter() {
//是否使用过滤器,true是使用过滤器
return true;
}
public Object run() throws ZuulException {
//过滤器具体执行的逻辑
System.out.println("过滤器执行了");
//获取requestContext容器
RequestContext ctx = RequestContext.getCurrentContext();
//获取request和response
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response = ctx.getResponse();
//声明存放加密后的数据变量
String requestData = null;
//声明存放解密后的数据变量
String decryptData = null;
try {
//通过request获取inputStream
ServletInputStream inputStream = request.getInputStream();
//从inputStream中得到加密后的数据
requestData = StreamUtils.copyToString(inputStream, Charsets.UTF_8);
System.out.println(requestData);
//对加密后的数据进行解密操作
if (!Strings.isNullOrEmpty(requestData)) {
decryptData = rsaService.RSADecryptDataPEM(requestData, RsaKeys.getServerPrvKeyPkcs8());
System.out.println(decryptData);
}
//把解密后的数据进行转发,需要放到request中
if (!Strings.isNullOrEmpty(decryptData)) {
//获取解密后的数据的字节数组
byte[] bytes = decryptData.getBytes();
//使用RequestContext进行数据的转发
ctx.setRequest(new HttpServletRequestWrapper(request) {
public ServletInputStream getInputStream() throws IOException {
return new ServletInputStreamWrapper(bytes);
}
public int getContentLength() {
return bytes.length;
}
public long getContentLengthLong() {
return bytes.length;
}
});
}
//需要设置request的请求头中的content-Type为json数据,如果不设置api接口模块就需要进行url转码操作
ctx.addZuulRequestHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}7.将openssl生成的公钥和私钥添加进RsaKeys中
公钥变量:
private static final String serverPubKey私钥变量:
private static final String serverPrvKeyPkcs88.测试请求参数加解密微服务
启动tensquare_eureka,tensquare_article,tensquare_encrypt,使用EncryptTest类加密请求参数,然后使用postman进行接口调用测试

