文章目录
- 简介
- 实现方式
- 主要代码
- 调用方法
-
1、简介
-
2、实现方式
-
3、服务端主要代码
-
4、客户端主要代码
-
5、调用方式
简介
- 为什么不使用SSL证书?
1、服务器运行在专网环境,不能访问互联网。证书有有效期,CA机构规定,证书有效期最多2年。在客户的专网环境里更新和维护证书就会增加运营成本。 - 实现逻辑?
参照SSL的实现逻辑,与SSL的区别就是SSL的公钥是通过证书下发的,这里为了免去证书维护的麻烦,公钥直接下发给客户端。SSL处理流程如下:
详情可参见本人另一篇博文:HTTPS请求过程
实现方式
- 使用装饰者模式对原有的websocket模块进行功能扩展。
- 服务端基于 YeautyYE 基于Netty的开源项目netty-websocket-spring-boot-starter(轻量级、高性能的WebSocket框架)在应用层封装加密过程,客户端接入时进行密钥交换。
重写onOpen、onClose方法,对客户端连接和断开进行处理。重写消息接收和消息发送方法,对消息进行加解密和过滤。 - 客户端基于Java-WebSocket,重写消息接收和消息发送方法,实现数据加解密。
主要代码
- 服务端主要代码:
/**
* 安全的websocket服务
*/
public abstract class SercurityWebsocketService {
public static final String CHAR_SET = "UTF-8";
//存放RSE密钥对
static Map<String, String> keyPairMap = null;
//存放客户端发过来的AES密钥,key为client的session key
static Map<String, String> clientKeysMap = new ConcurrentHashMap<>();
static {
try {
keyPairMap = RSACoder.genKeyPair();
System.out.println("公钥:" + keyPairMap.get(RSACoder.PUBLIC_KEY));
System.out.println("私钥:" + keyPairMap.get(RSACoder.PRIVATE_KEY));
} catch (Exception e) {
e.printStackTrace();
System.out.println("密钥对初始化失败:" + e.getMessage());
}
}
//服务启动,生成RSA密钥对,保存在本地。
//客户端连接时,给客户端发送公开密钥+clientId(sessionId)。
//接收客户端发送的随机码
//解密随机码,并且使用sessionId作为client_id,保存。
@OnOpen
public void OnOpen(Session session, HttpHeaders headers) throws IOException {
//1、直接把密钥发给客户端。
SecurityMessageEntity entity = new SecurityMessageEntity();
entity.setMsgType(SecurityMessageEntity.MSG_TYPE_PUBLIC_KEY);
entity.setSercurityContent(keyPairMap.get(RSACoder.PUBLIC_KEY));
send(session,keyPairMap.get(RSACoder.PUBLIC_KEY),
SecurityMessageEntity.SECURITY_TYPE_RSE,
SecurityMessageEntity.MSG_TYPE_PUBLIC_KEY);
//调用子类的实现
this.onOpen(session, headers);
}
@OnClose
public void OnClose(Session session) throws IOException {
//客户端关闭,移除clientKey
clientKeysMap.remove(session.id().asLongText());
System.out.println("移除clientKey:" + session.id().asLongText());
//调用子类的实现
this.onClose(session);
}
@OnError
public void OnError(Session session, Throwable throwable) {
clientKeysMap.remove(session.id().asLongText());
throwable.printStackTrace();
onError(session, throwable);
}
@OnMessage
public void OnMessage(Session session, String message) {
try {
String jsonMessage = dencryptFromBase64String(message);
onMessageHandle(session, jsonMessage);
} catch (Exception e) {
session.sendText("密钥解析失败:" + e.getMessage());
}
}
/**
* websocket处理接收到的消息,解析后得到消息体内容,调用抽象方法OnMessage。
* 1、验证是否有securityType,如果有securityType,按照密文类型,进行解密。
* 2、如果没有securityType,当成明文处理
*
* @param session
* @param msg
*/
private void onMessageHandle(Session session, String msg) throws Exception {
JSONObject jsonObject = JSON.parseObject(msg);
if (jsonObject.containsKey("securityType")) {
SecurityMessageEntity keyEntity = JSON.parseObject(msg, SecurityMessageEntity.class);
switch (keyEntity.getSecurityType()) {
case SecurityMessageEntity.SECURITY_TYPE_RSE:
//RES加密方式,为客户端传过来的client key,用于aes加解密。
String aesKey = RSACoder.decrypt(keyEntity.getSercurityContent(), keyPairMap.get(RSACoder.PRIVATE_KEY));
clientKeysMap.put(session.id().asLongText(), aesKey);
System.out.println("客户端id:" + session.id().asLongText());
System.out.println("aes key:" + aesKey);
break;
case SecurityMessageEntity.SECURITY_TYPE_AES:
//AES加密方式。
String key = clientKeysMap.get(session.id().asLongText());
if (StringUtils.isEmpty(key)) {
System.out.println("解析消息失败,找不到对应的clientKey");
session.sendText("解析消息失败,找不到对应的clientKey");
} else {
//使用AES进行解密
String decryptMessage = AesHelper.decrypt(keyEntity.getSercurityContent(), key);
this.onMessage(session, decryptMessage);
}
default:
break;
}
} else {
this.onMessage(session, msg);
}
}
/**
* 解析客户端穿过来的数据
* 1、把客户端传过来的base64字符串进行解密,得到json字符串,
* 2、json字符串转换为key
* 3、把加密的密钥进行解密。
* 4、返回
*
* @param str
* @return
* @throws Exception
*/
public static String dencryptFromBase64String(String str) throws Exception {
byte[] bytes = Base64.decodeBase64(str);
String jsonString = new String(bytes, CHAR_SET);
return jsonString;
}
/**
* 发送消息
*
* @param session
* @param text
* @throws NotYetConnectedException
*/
public void send(Session session, String text) throws NotYetConnectedException {
try {
String key = clientKeysMap.get(session.id().asLongText());
String encryptMessage = AesHelper.encrypt(text, key);
send(session, encryptMessage, SecurityMessageEntity.SECURITY_TYPE_AES, null);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
System.out.println("消息发送失败:" + e.getMessage());
}
}
private void send(Session session, String text, String securityType, String msgType) throws UnsupportedEncodingException {
SecurityMessageEntity entity = new SecurityMessageEntity();
entity.setSecurityType(securityType);
entity.setMsgType(msgType);
entity.setSercurityContent(text);
String jsonMessage = JSONObject.toJSONString(entity);
String msgContent = Base64.encodeBase64String(jsonMessage.getBytes(CHAR_SET));
session.sendText(msgContent);
}
/**收到消息回调方法
* @param session 会话session
* @param message
*/
abstract void onMessage(Session session, String message);
/**
* 打开连接回调方法
* @param session 会话session
* @param headers
*/
abstract void onOpen(Session session, HttpHeaders headers);
/**连接出错回调方法
* @param session 会话session
* @param throwable
*/
abstract void onError(Session session, Throwable throwable);
/**连接关闭回调方法
* @param session 会话session
* @throws IOException
*/
abstract void onClose(Session session) throws IOException;
}
- 客户端主要代码:
public abstract class SercurityWebSocketClient extends WebSocketClient {
private static String CHAR_SET = "UTF-8";
private static String clientKey;
public SercurityWebSocketClient(String url) throws URISyntaxException {
super(new URI(url));
}
@Override
public void onOpen(ServerHandshake serverHandshake) {
System.out.println("握手...");
onConnect(serverHandshake);
}
@Override
public void onMessage(String s) {
try {
onMessageHandle(s);
} catch (Exception e) {
e.printStackTrace();
System.out.println("消息解析出错:" + e.getMessage());
}
}
@Override
public void onClose(int i, String s, boolean b) {
onDisconnect(i,s,b);
}
@Override
public void onError(Exception e) {
onException(e);
}
/**
* 消息处理
*
* @param msg
* @throws Exception
*/
private void onMessageHandle(String msg) throws Exception {
//1、使用base64解码
String jsonMsg = dencryptFromBase64String(msg);
//2、验证是否是Public key。
JSONObject jsonObject = JSONObject.parseObject(jsonMsg);
if (SecurityMessageEntity.MSG_TYPE_PUBLIC_KEY.equals(jsonObject.getString("msgType"))) {
//说明是服务器发送过来的公钥。随机生成client_key,
clientKey = createClientKey();
String encrypt = RSACoder.encrypt(clientKey, jsonObject.getString("sercurityContent"));
System.out.println(String.format("明文:%s,非对称加密结果:%s", clientKey, encrypt));
send(encrypt,SecurityMessageEntity.SECURITY_TYPE_RSE,null);
} else {
//否则是普通加密的消息。使用本地的clientKey解密
String jsonContent = AesHelper.decrypt(jsonObject.getString("sercurityContent"), clientKey);
onReceiveMessage(jsonContent);
}
}
/**
* 创建客户端的密钥,并使用public key进行加密。
*
* @return
*/
private String createClientKey() throws Exception {
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
return uuid;
}
/**
* 把base64的字符串解析为json字符串
*
* @param str
* @return
* @throws Exception
*/
public static String dencryptFromBase64String(String str) throws Exception {
byte[] bytes = Base64.decodeBase64(str);
String jsonString = new String(bytes, CHAR_SET);
return jsonString;
}
/**重写发送方法,消息发送之前先进行加密
* @param text
* @throws NotYetConnectedException
*/
@Override
public void send(String text) throws NotYetConnectedException {
try {
String encryptMessage = AesHelper.encrypt(text, clientKey);
send(encryptMessage, SecurityMessageEntity.SECURITY_TYPE_AES,null);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
System.out.println("消息发送失败:" + e.getMessage());
}
}
private void send(String text, String securityType,String msgType) throws UnsupportedEncodingException {
SecurityMessageEntity entity = new SecurityMessageEntity();
entity.setSecurityType(securityType);
entity.setMsgType(msgType);
entity.setSercurityContent(text);
String jsonMessage = JSONObject.toJSONString(entity);
String msgContent = Base64.encodeBase64String(jsonMessage.getBytes(CHAR_SET));
super.send(msgContent);
}
/**收到消息
* @param s
*/
protected abstract void onReceiveMessage(String s);
/**连接断开
* @param i
* @param s
* @param b
*/
protected abstract void onDisconnect(int i, String s, boolean b);
/**连接异常
* @param e
*/
protected abstract void onException(Exception e);
/**
* 连接成功
* @param serverHandshake
*/
protected abstract void onConnect(ServerHandshake serverHandshake);
}
调用方法
跟原框架调用逻辑类似,自定义类继承服务端(SercurityWebsocketService)或者客户端(SercurityWebSocketClient)类,实现其中的抽象方法即可。发送消息调用super类中的send方法即可。
- 服务端调用示例:
@ServerEndpoint(prefix = "netty-websocket",path = "/sercurity" )
@Component
public class BusinessServiceImpl extends SercurityWebsocketService {
@Override
void onMessage(Session session, String message) {
System.out.println("接收到客户端消息:"+message);
send(session,"您好,client,已经收到您的消息。");
}
@Override
void onOpen(Session session, HttpHeaders headers) {
System.out.println("调用子方法");
}
@Override
void onError(Session session, Throwable throwable) {
}
@Override
void onClose(Session session) throws IOException {
}
}
- 客户端调用示例:
try {
SercurityWebSocketClient socketClient=new SercurityWebSocketClient("ws://127.0.0.1:9002/sercurity") {
@Override
public void onReceiveMessage(String s) {
}
@Override
protected void onDisconnect(int i, String s, boolean b) {
}
@Override
protected void onException(Exception e) {
}
@Override
protected void onConnect(ServerHandshake serverHandshake) {
}
};
socketClient.connect();
while (!socketClient.getReadyState().equals(WebSocket.READYSTATE.OPEN)) {
System.out.println("还没有打开");
Thread.sleep(2000);
}
System.out.println("建立websocket连接");
socketClient.send("我是client");
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
} catch (URISyntaxException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}