第 7 篇 : RSA消息加密

说明

写到这里,我忽然意识到,任何事情要想做好都不是一蹴而就的,都需要耗费大量的时间和人力。至今为止,我对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发送消息请求
服务端收到消息
Bb收到消息
测试完毕, 日志打印可以去掉了

写在最后

关于秘钥生成以及传输问题, 只要是网络传输就会有风险。
(1) 由客户端生成公、私钥, 只将公钥公布。泄密风险最低, 全世界只此一份私钥, 别人发的消息, 除非机器丢了, 否则不可能泄密。风险点: 如果秘钥丢失, 在新公钥没有替换之前, 别人发送的消息将永远丢失, 且服务端不能够监控客户端的消息, 危险性行为大大提高
(2) 由服务端生成公、私钥, 将其私钥分发给对应客户端, 公钥公布。因为有网络传输, 所以还是有风险泄漏私钥的, 但是有证书加上登录态再分发私钥, 看下来应该问题不大, 而且可以更新, 出现泄漏可以重新生成分发。分发时也可用过短信方式将秘钥发给用户, 由用户自行保存到机器上。还有, 如果某用户被举报或有违规行为时, 还可以在验签时, 加入解密用户消息的逻辑, 防祸于未然。

  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哈哈兽0026

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值