说明
写到这里,我忽然意识到,任何事情要想做好都不是一蹴而就的,都需要耗费大量的时间和人力。至今为止,我对Netty只是写了一点点东西,还有很多需要学习和掌握, 爱因斯坦的圆圈理论让人深思,只能说,学无止境。加密的知识跟netty关系并不大, 而且将使用RSA非对称加密, 大概思路: 由客户端Aa使用自己的私钥进行消息签名, 使用Bb的公钥进行消息加密, 发送到服务端, 由服务端进行验签, 验签通过后, 再由服务端发送给客户端Bb, Bb使用自己的私钥进行消息解密
1. 客户端改造
1.1 utils包下新增 RsaUtils类
package com.hahashou.netty.client.utils;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* @description:
* @author: 哼唧兽
* @date: 9999/9/21
**/
@Slf4j
public class RsaUtils {
private static Base64.Encoder ENCODER = Base64.getEncoder();
private static final String ALGORITHMS = "RSA";
private static final String ALGORITHM = "SHA256withRSA";
private static KeyFactory KEY_FACTORY;
private static Signature SIGNATURE;
private static Cipher CIPHER;
static {
try {
KEY_FACTORY = KeyFactory.getInstance(ALGORITHMS);
SIGNATURE = Signature.getInstance(ALGORITHM);
CIPHER = Cipher.getInstance(ALGORITHMS);
} catch (GeneralSecurityException exception) {
System.out.println("异常: " + exception.getMessage());
}
}
public static void main(String[] args) throws NoSuchAlgorithmException {
keysInit("Aa");
System.out.println("==================================================");
keysInit("Bb");
}
/**
* 生成用户的公钥、私钥
* @param userCode
* @throws NoSuchAlgorithmException
*/
private static void keysInit(String userCode) throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHMS);
// 想加密性更好可以设置成2048
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
Map<String, String> map = new HashMap<>(8);
map.put(key(userCode, 0), ENCODER.encodeToString(keyPair.getPublic().getEncoded()));
map.put(key(userCode, 1), ENCODER.encodeToString(keyPair.getPrivate().getEncoded()));
System.out.println(map.toString());
}
/**
* 生成key
* @param code
* @param type 0: 公钥; 1: 私钥
* @return
*/
private static String key(String code, int type) {
return type == 0 ? code + "_PUBLIC" : code + "_PRIVATE";
}
/**
* 用户签名
* @param userCode
* @param privateKeyBytes
* @return
* @throws GeneralSecurityException
*/
public static String sign(String userCode, byte[] privateKeyBytes) throws GeneralSecurityException {
SIGNATURE.initSign(KEY_FACTORY.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes)));
SIGNATURE.update(userCode.getBytes());
return ENCODER.encodeToString(SIGNATURE.sign());
}
/**
* 加密消息
* @param text
* @param publicKeyBytes
* @return
* @throws GeneralSecurityException
*/
public static String encrypt(String text, byte[] publicKeyBytes) throws GeneralSecurityException {
CIPHER.init(Cipher.ENCRYPT_MODE, KEY_FACTORY.generatePublic(new X509EncodedKeySpec(publicKeyBytes)));
return ENCODER.encodeToString(CIPHER.doFinal(text.getBytes(StandardCharsets.UTF_8)));
}
/**
* 解密消息
* @param text
* @param privateKeyBytes
* @return
*/
public static String decrypt(String text, byte[] privateKeyBytes) {
String result = "";
try {
CIPHER.init(Cipher.DECRYPT_MODE, KEY_FACTORY.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes)));
result = new String(CIPHER.doFinal(Base64.getDecoder().decode(text)), StandardCharsets.UTF_8);
} catch (GeneralSecurityException exception) {
log.error("解密失败: {}", exception.getMessage());
}
return result;
}
}
1.2 通过RsaUtils的main方法生成2对公、私钥放到ClientStatic类中(或直接使用我的)
public static final String Aa_PUBLIC = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCWMKpS0HPjTG3rcsIYUCeaQl/ErJtaBuwZqtcKpySqtfOg6B6xfLAeWsp0Mym12eGGVeLdlp+PT/jffgIr60ZdBIoyH4lNyBGB0GbH/l5J/VRyqSN2YiuZBD7RwFCRl36Noj0ySBqisaahDviXwh7CKXhfr0LcNDB8lVREod0zIQIDAQAB";
public static final String Aa_PRIVATE = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAJYwqlLQc+NMbetywhhQJ5pCX8Ssm1oG7Bmq1wqnJKq186DoHrF8sB5aynQzKbXZ4YZV4t2Wn49P+N9+AivrRl0EijIfiU3IEYHQZsf+Xkn9VHKpI3ZiK5kEPtHAUJGXfo2iPTJIGqKxpqEO+JfCHsIpeF+vQtw0MHyVVESh3TMhAgMBAAECgYEAjC6sK1PpdvR1fFfGlk7qR+8/2CCLeAISCPsOcCEF9liSJ1PAokURVaPEZ6UBf3z4JRyw/caC847faisA9+FH8D3HlEGEb3rsJ4HoWGxkfTsarEbtPbHvdWqw26+eZ3EsHdf7g4xGpjtuDotaYs4ELsQGZudPN39eJSS/mL9zNWECQQD4tQed9AUufRFc+kDKT/ZvzHpWLXB5yPddyHWQeWjEctNDHTAUVnETDNev5iHCOmmUlpbOzIAXTNolIm4j2EX9AkEAmpgY91kIBM+Y8fsPu8x2qKi5kQ108Ne3IJkRxCRNaXVpyJ/qEE0m44K8RCiGHmWcXsoVwe163LmytuK1eeSY9QJAKFQahxdhm7c2EJCX3vZ2bIyIrd6yZV0cF34A6kt1nJ1N+o0KFdIqhb9IXkJ/6OHV6v08OQ5aGu0gVnMtzuwr9QJAEQktxyQihBU0b4YRJ8rSUKe3O0rWViwPXCJCGPE/Lp3nuFoW+xDldjDT+lbU4MilwLRYTXSUE3rLPOgiw3nzeQJAGs93QJ58oWg7WXmzFadrJ0llITn2C1xhP4sAElfTA9RsHt47wWkEVsH/ptGrWb4A9ZMOPkqUhE6u2TDHBh+EDw==";
public static final String Bb_PUBLIC = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeNpaZtG0dZXjn6sdcjwsAnthHMiiG/RTGs2Aa4MJzZHPqwPRQSOhzYUrntb9p/tDK0Osdo4enhLp+teWRB4Uag6mq8cg9iCsDENm1ySROAeY3LoqjTV/a4uF6QSOUEkaWDosPOXMFAiuAU6sUfDCywBaMjoGP6NeGOSO5ox3VJwIDAQAB";
public static final String Bb_PRIVATE = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAJ42lpm0bR1leOfqx1yPCwCe2EcyKIb9FMazYBrgwnNkc+rA9FBI6HNhSue1v2n+0MrQ6x2jh6eEun615ZEHhRqDqarxyD2IKwMQ2bXJJE4B5jcuiqNNX9ri4XpBI5QSRpYOiw85cwUCK4BTqxR8MLLAFoyOgY/o14Y5I7mjHdUnAgMBAAECgYAOlWeSaYA5WnYnoouX65OPDhVPkr8Lml6E5lnwgFFMQ7EvrXOXxvCuWgSGkUlAov1qBJH3nHBPr7tlHK05jiDlqKFTRJMI7a2vQs0i4BGEj8ax0jbhV/Vt+0Ug1zgUHPTYQU2Fv+Mnnjqdft9uXUjwj5NxX46IgpvReqYHr2eMiQJBANOGW3vdu2PDyNh/SrWRw1MDoRDhTaMP4Ayqa9gejja28S5enIpoVBoLNj1t5F10WURa645bcerDzLtkWBX+4UsCQQC/eqdltxVqLB0r1cqrpatiw9ZyuZVzGsyS34r78cUE7EV6hTnL3LucpISBEyPM5IeRWFdvGJiVWW5hFD+L4c4VAkEAlCRWMBMj6YQ2RwInhbCXlq1FAbh5kklNBjHZI9yKh2Fq2qnigsD8ndzaWP184cLZvhjbPrFmwB/vZBKr6oO+rwJAXBMEz9p8B7PyyxNhA60EftehFUW8Yb8vRCkOUhxuGvHqbwIFSsx3wtkxhkfH3Uy/C9spIBj5tkds1m3AKOmKCQJBAL0mKkAcZ1jnsM/IkBGlU+JulAcC69MMre1rPR26IrX86C8Ixe5iT4kMF5ZUeu9V8AMqCbz+9P8AuCnSuSylQ1I=";
1.3 修改 Message类
package com.hahashou.netty.client.config;
import com.alibaba.fastjson.JSON;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.CharsetUtil;
import lombok.Data;
import lombok.Getter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* @description:
* @author: 哼唧兽
* @date: 9999/9/21
**/
@Data
public class Message {
/** 发送者用户签名 */
private String userSign;
/** 发送者用户code */
private String userCode;
/** 接收者用户code */
private String friendUserCode;
/** 连接时专用 */
private String channelId;
/** 消息类型 */
private Integer type;
public enum TypeEnum {
TEXT(0, "文字", "", new ArrayList<>()),
IMAGE(1, "图片", "image", Arrays.asList("bmp", "gif", "jpeg", "jpg", "png")),
VOICE(2, "语音", "voice", Arrays.asList("mp3", "amr", "flac", "wma", "aac")),
VIDEO(3, "视频", "video", Arrays.asList("mp4", "avi", "rmvb", "flv", "3gp", "ts", "mkv")),
;
@Getter
private Integer key;
@Getter
private String describe;
@Getter
private String bucketName;
@Getter
private List<String> formatList;
TypeEnum(int key, String describe, String bucketName, List<String> formatList) {
this.key = key;
this.describe = describe;
this.bucketName = bucketName;
this.formatList = formatList;
}
public static TypeEnum select(String format) {
TypeEnum result = null;
for (TypeEnum typeEnum : TypeEnum.values()) {
if (typeEnum.getFormatList().contains(format)) {
result = typeEnum;
break;
}
}
return result;
}
}
/** 文字或文件的全路径名称 */
private String text;
public static ByteBuf transfer(Message message) {
return Unpooled.copiedBuffer(JSON.toJSONString(message), CharsetUtil.UTF_8);
}
/**
* 生成指定长度的随机字符串
* @param length
* @return
*/
public static String randomString (int length) {
if (length > 64) {
length = 64;
}
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i + "");
}
for (char i = 'A'; i <= 'Z'; i++) {
list.add(String.valueOf(i));
}
for (char i = 'a'; i <= 'z'; i++) {
list.add(String.valueOf(i));
}
list.add("α");
list.add("ω");
Collections.shuffle(list);
String string = list.toString();
return string.replace("[", "")
.replace("]", "")
.replace(", ", "")
.substring(0, length);
}
}
1.4 修改 ClientService接口
package com.hahashou.netty.client.service;
import com.hahashou.netty.client.config.Message;
import javax.servlet.http.HttpServletRequest;
import java.security.GeneralSecurityException;
/**
* @description:
* @author: 哼唧兽
* @date: 9999/9/21
**/
public interface ClientService {
/**
* 发送消息
* @param dto
* @throws GeneralSecurityException
*/
void send(Message dto) throws GeneralSecurityException;
/**
* 上传
* @param userCode
* @param httpServletRequest
* @return
*/
Message upload(String userCode, HttpServletRequest httpServletRequest);
/**
* 下载链接
* @param fileName
* @return
*/
String link(String fileName);
}
1.5 修改 ClientServiceImpl类
package com.hahashou.netty.client.service.impl;
import com.hahashou.netty.client.config.ClientStatic;
import com.hahashou.netty.client.config.Message;
import com.hahashou.netty.client.config.NettyClientHandler;
import com.hahashou.netty.client.service.ClientService;
import com.hahashou.netty.client.utils.MinioUtils;
import com.hahashou.netty.client.utils.RsaUtils;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.security.GeneralSecurityException;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.*;
/**
* @description:
* @author: 哼唧兽
* @date: 9999/9/21
**/
@Service
@Slf4j
public class ClientServiceImpl implements ClientService {
@Resource
private MinioUtils minioUtils;
@Override
public void send(Message dto) throws GeneralSecurityException {
Channel channel = NettyClientHandler.CHANNEL;
if (channel == null) {
log.error("服务端已下线, 准备存储到本地");
return;
}
Base64.Decoder decoder = Base64.getDecoder();
// Aa私钥签名
String sign = RsaUtils.sign(ClientStatic.USER_CODE, decoder.decode(ClientStatic.Aa_PRIVATE));
dto.setUserSign(sign);
// Bb公钥加密
String encrypt = RsaUtils.encrypt(dto.getText(), decoder.decode(ClientStatic.Bb_PUBLIC));
dto.setText(encrypt);
channel.writeAndFlush(Message.transfer(dto));
}
@Override
public Message upload(String userCode, HttpServletRequest httpServletRequest) {
Message result = new Message();
MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) httpServletRequest;
List<MultipartFile> multipartFileList = multipartHttpServletRequest.getFiles("file");
if (!CollectionUtils.isEmpty(multipartFileList)) {
MultipartFile multipartFile = multipartFileList.get(0);
String originalFilename = multipartFile.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
Message.TypeEnum typeEnum = Message.TypeEnum.select(suffix);
if (typeEnum != null) {
String fileName = generateFileName(suffix);
String multiLevelFolders = userCode + "/"
+ LocalDate.now().toString().replace("-", "") + "/";
minioUtils.upload(typeEnum.getBucketName(), multiLevelFolders, fileName, multipartFile);
result.setType(typeEnum.getKey());
result.setText(multiLevelFolders + fileName);
}
}
return result;
}
public static String generateFileName(String suffix) {
LocalTime now = LocalTime.now();
return now.toString().replace(":", "")
.replace(".", "-") + Message.randomString(4) + "." + suffix;
}
@Override
public String link(String fileName) {
String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
Message.TypeEnum typeEnum = Message.TypeEnum.select(suffix);
if (typeEnum != null) {
return minioUtils.preview(fileName, typeEnum.getBucketName());
}
return null;
}
}
1.6 修改 ClientController类
package com.hahashou.netty.client.controller;
import com.alibaba.fastjson.JSON;
import com.hahashou.netty.client.config.*;
import com.hahashou.netty.client.service.ClientService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.security.GeneralSecurityException;
/**
* @description:
* @author: 哼唧兽
* @date: 9999/9/21
**/
@RestController
@RequestMapping("/client")
@Slf4j
public class ClientController {
@Resource
private ClientService clientService;
@PostMapping("/send")
public String send(@RequestBody Message dto) throws GeneralSecurityException {
clientService.send(dto);
return "success";
}
@GetMapping("/upload/{userCode}")
public String upload(@PathVariable String userCode, final HttpServletRequest httpServletRequest) {
if (StringUtils.isEmpty(userCode)) {
return "userCode is null";
}
Message upload = clientService.upload(userCode, httpServletRequest);
return JSON.toJSONString(upload);
}
@GetMapping("/link")
public String link(@RequestParam String fileName) {
// 如果Bucket包含多级目录, fileName为Bucket下文件的全路径名
if (StringUtils.isEmpty(fileName)) {
return "fileName is null";
}
return clientService.link(fileName);
}
@GetMapping("/start")
public void start() {
ClientStatic.CLIENT = new NettyClient();
}
@GetMapping("/stop")
public String stopClient() {
ClientStatic.CLIENT.stopClient();
ClientStatic.CLIENT = null;
return "success";
}
}
1.7 修改 NettyClientHandler类
package com.hahashou.netty.client.config;
import com.alibaba.fastjson.JSON;
import com.hahashou.netty.client.utils.RsaUtils;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.Base64;
/**
* @description:
* @author: 哼唧兽
* @date: 9999/9/21
**/
@ChannelHandler.Sharable
@Slf4j
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
public static Channel CHANNEL;
@Override
public void channelActive(ChannelHandlerContext ctx) {
CHANNEL = ctx.channel();
log.info("客户端 " + ClientStatic.USER_CODE + " 上线");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
CHANNEL = null;
log.error("服务端已下线, 准备重连");
ClientStatic.CLIENT = new NettyClient();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg != null) {
Message message = JSON.parseObject(msg.toString(), Message.class);
String channelId = message.getChannelId(),
text = message.getText();
if (StringUtils.hasText(channelId)) {
Channel channel = ctx.channel();
message.setUserCode(ClientStatic.USER_CODE);
channel.writeAndFlush(Message.transfer(message));
} else if (StringUtils.hasText(text)) {
log.info("加密消息: {}", text);
text = RsaUtils.decrypt(text, Base64.getDecoder().decode(ClientStatic.Bb_PRIVATE));
log.info("收到" + message.getUserCode() + "消息: {}", text);
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
CHANNEL = null;
}
}
1.8 打包2个jar, 用户Code分别是Aa、Bb, 分别放到163、164
2. 服务端改造
2.1 新增utils包, 新增 RsaUtils类
package com.hahashou.netty.server.utils;
import lombok.extern.slf4j.Slf4j;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* @description:
* @author: 哼唧兽
* @date: 9999/9/21
**/
@Slf4j
public class RsaUtils {
private static final String ALGORITHMS = "RSA";
private static final String ALGORITHM = "SHA256withRSA";
private static KeyFactory KEY_FACTORY;
private static Signature SIGNATURE;
static {
try {
KEY_FACTORY = KeyFactory.getInstance(ALGORITHMS);
SIGNATURE = Signature.getInstance(ALGORITHM);
} catch (GeneralSecurityException exception) {
System.out.println("异常: " + exception.getMessage());
}
}
/**
* 验证用户签名
* @param userCode
* @param sign
* @param publicKeyBytes
* @return
*/
public static boolean verifySign(String userCode, String sign, byte[] publicKeyBytes) {
log.info("用户code: {}", userCode);
log.info("用户签名: {}", sign);
boolean verify = false;
try {
SIGNATURE.initVerify(KEY_FACTORY.generatePublic(new X509EncodedKeySpec(publicKeyBytes)));
SIGNATURE.update(userCode.getBytes());
verify = SIGNATURE.verify(Base64.getDecoder().decode(sign));
} catch (GeneralSecurityException exception) {
log.error("验签失败: {}", exception.getMessage());
}
return verify;
}
}
2.2 将客户端的2对公、私钥放到NettyStatic类中
2.3 修改 Message类
package com.hahashou.netty.server.config;
import com.alibaba.fastjson.JSON;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.CharsetUtil;
import lombok.Data;
import lombok.Getter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* @description:
* @author: 哼唧兽
* @date: 9999/9/21
**/
@Data
public class Message {
/** 广播秘钥 */
private String secretKey;
/** 发送者用户签名 */
private String userSign;
/** 发送者用户code */
private String userCode;
/** 中转的服务端Id */
private String serverId;
/** 接收者用户code */
private String friendUserCode;
/** 连接时专用 */
private String channelId;
/** 消息类型 */
private Integer type;
public enum TypeEnum {
TEXT(0, "文字", "", new ArrayList<>()),
IMAGE(1, "图片", "image", Arrays.asList("bmp", "gif", "jpeg", "jpg", "png")),
VOICE(2, "语音", "voice", Arrays.asList("mp3", "amr", "flac", "wma", "aac")),
VIDEO(3, "视频", "video", Arrays.asList("mp4", "avi", "rmvb", "flv", "3gp", "ts", "mkv")),
;
@Getter
private Integer key;
@Getter
private String describe;
@Getter
private String bucketName;
@Getter
private List<String> formatList;
TypeEnum(int key, String describe, String bucketName, List<String> formatList) {
this.key = key;
this.describe = describe;
this.bucketName = bucketName;
this.formatList = formatList;
}
public static TypeEnum select(String format) {
TypeEnum result = null;
for (TypeEnum typeEnum : TypeEnum.values()) {
if (typeEnum.getFormatList().contains(format)) {
result = typeEnum;
break;
}
}
return result;
}
}
/** 文字或文件的全路径名称 */
private String text;
public static ByteBuf transfer(Message message) {
return Unpooled.copiedBuffer(JSON.toJSONString(message), CharsetUtil.UTF_8);
}
/**
* 生成指定长度的随机字符串
* @param length
* @return
*/
public static String randomString (int length) {
if (length > 64) {
length = 64;
}
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i + "");
}
for (char i = 'A'; i <= 'Z'; i++) {
list.add(String.valueOf(i));
}
for (char i = 'a'; i <= 'z'; i++) {
list.add(String.valueOf(i));
}
list.add("α");
list.add("ω");
Collections.shuffle(list);
String string = list.toString();
return string.replace("[", "")
.replace("]", "")
.replace(", ", "")
.substring(0, length);
}
}
2.4 修改 NettyServerHandler类
package com.hahashou.netty.server.config;
import com.alibaba.fastjson.JSON;
import com.hahashou.netty.server.utils.RsaUtils;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
/**
* @description:
* @author: 哼唧兽
* @date: 9999/9/21
**/
@Component
@ChannelHandler.Sharable
@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
@Value("${netty.server.id}")
private String serverId;
@Value("${netty.server.port}")
private int port;
public static String SERVER_PREFIX = "netty-server-";
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private ValueOperations<String, Object> redisOperation;
@Override
public void channelActive(ChannelHandlerContext ctx) {
Channel channel = ctx.channel();
String channelId = channel.id().asLongText();
log.info("客户端连接, channelId : {}", channelId);
NettyStatic.CHANNEL.put(channelId, channel);
Message message = new Message();
message.setChannelId(channelId);
channel.writeAndFlush(Message.transfer(message));
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
String channelId = ctx.channel().id().asLongText();
log.info("客户端断开连接, channelId : {}", channelId);
NettyStatic.CHANNEL.remove(channelId);
for (Map.Entry<String, String> entry : NettyStatic.USER_CHANNEL.entrySet()) {
if (entry.getValue().equals(channelId)) {
redisTemplate.delete(entry.getKey());
break;
}
}
redisOperation.set(RedisConfig.key(serverId, port + ""), CONNECT_NUMBER.decrementAndGet());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg != null) {
Message message = JSON.parseObject(msg.toString(), Message.class);
String userCode = message.getUserCode(),
channelId = message.getChannelId(),
friendUserCode = message.getFriendUserCode();
if (StringUtils.hasText(userCode) && StringUtils.hasText(channelId)) {
connect(userCode, channelId);
} else if (StringUtils.hasText(message.getText())) {
String userSign = message.getUserSign();
boolean verifySign = RsaUtils.verifySign(userCode, userSign, Base64.getDecoder().decode(NettyStatic.Aa_PUBLIC));
if (verifySign) {
Object code = redisOperation.get(friendUserCode);
if (code != null) {
String queryServerId = code.toString();
message.setServerId(serverId.equals(queryServerId) ? null : queryServerId);
if (StringUtils.hasText(friendUserCode)) {
sendOtherClient(message);
} else {
sendAdmin(ctx.channel(), message);
}
} else {
offlineMessage(friendUserCode, message);
}
} else {
log.error("通知用户" + userCode + ", 您的秘钥已泄露, 请及时更新");
}
}
}
}
/** 后续还可以做一个最大连接数的限制, 到达后通知monitor, 使后续客户端不再连接此服务端 */
public static AtomicLong CONNECT_NUMBER = new AtomicLong(0);
/**
* 建立连接
* @param userCode
* @param channelId
*/
private void connect(String userCode, String channelId) {
log.info("{} 连接", userCode);
NettyStatic.USER_CHANNEL.put(userCode, channelId);
if (!userCode.startsWith(SERVER_PREFIX)) {
redisOperation.set(userCode, serverId);
}
redisOperation.set(RedisConfig.key(serverId, port + ""), CONNECT_NUMBER.incrementAndGet());
}
/**
* 发送给其他客户端
* @param message
*/
private void sendOtherClient(Message message) {
String friendUserCode = message.getFriendUserCode(),
serverId = message.getServerId();
String queryChannelId;
if (StringUtils.hasText(serverId)) {
log.debug("向" + serverId + " 进行转发");
queryChannelId = NettyStatic.USER_CHANNEL.get(serverId);
} else {
queryChannelId = NettyStatic.USER_CHANNEL.get(friendUserCode);
}
if (StringUtils.hasText(queryChannelId)) {
Channel channel = NettyStatic.CHANNEL.get(queryChannelId);
if (channel == null) {
offlineMessage(friendUserCode, message);
return;
}
channel.writeAndFlush(Message.transfer(message));
} else {
offlineMessage(friendUserCode, message);
}
}
/**
* 离线消息存储Redis
* @param friendUserCode
* @param message
*/
public void offlineMessage(String friendUserCode, Message message) {
// 1条message在redis中大概是100B, 1万条算1M, redis.conf的maxmemory设置的是256M
List<Message> messageList = new ArrayList<>();
Object offlineMessage = redisOperation.get(RedisConfig.OFFLINE_MESSAGE + friendUserCode);
if (offlineMessage != null) {
messageList = JSON.parseArray(offlineMessage.toString(), Message.class);
}
messageList.add(message);
redisOperation.set(RedisConfig.OFFLINE_MESSAGE + friendUserCode, JSON.toJSONString(messageList));
}
/**
* 发送给服务端
* @param channel
* @param message
*/
private void sendAdmin(Channel channel, Message message) {
message.setUserCode("ADMIN");
message.setText(LocalDateTime.now().toString());
channel.writeAndFlush(Message.transfer(message));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.info("客户端发生异常");
}
}
2.5 打包成jar, 放到161
3. 测试
启动服务端, 并启动客户端, 调用接口将Aa、Bb连接上。因为固定写死, 所以只能有Aa向Bb发送消息
测试完毕, 日志打印可以去掉了
写在最后
关于秘钥生成以及传输问题, 只要是网络传输就会有风险。
(1) 由客户端生成公、私钥, 只将公钥公布。泄密风险最低, 全世界只此一份私钥, 别人发的消息, 除非机器丢了, 否则不可能泄密。风险点: 如果秘钥丢失, 在新公钥没有替换之前, 别人发送的消息将永远丢失, 且服务端不能够监控客户端的消息, 危险性行为大大提高
(2) 由服务端生成公、私钥, 将其私钥分发给对应客户端, 公钥公布。因为有网络传输, 所以还是有风险泄漏私钥的, 但是有证书加上登录态再分发私钥, 看下来应该问题不大, 而且可以更新, 出现泄漏可以重新生成分发。分发时也可用过短信方式将秘钥发给用户, 由用户自行保存到机器上。还有, 如果某用户被举报或有违规行为时, 还可以在验签时, 加入解密用户消息的逻辑, 防祸于未然。