您现在的位置是:首页 >技术杂谈 >基于netty框架不使用SSL证书,实现websocket数据加密传输网站首页技术杂谈

基于netty框架不使用SSL证书,实现websocket数据加密传输

爱的不落叶 2024-06-17 10:14:33
简介基于netty框架不使用SSL证书,实现websocket数据加密传输

简介

  • 为什么不使用SSL证书?
    1、服务器运行在专网环境,不能访问互联网。证书有有效期,CA机构规定,证书有效期最多2年。在客户的专网环境里更新和维护证书就会增加运营成本。
  • 实现逻辑?
    参照SSL的实现逻辑,与SSL的区别就是SSL的公钥是通过证书下发的,这里为了免去证书维护的麻烦,公钥直接下发给客户端。SSL处理流程如下:image.png
    详情可参见本人另一篇博文: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();
        }
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。