物联网 基于netty核心实战-安全tls

物联网 基于netty核心实战-安全tls

简述

TLS(传输层安全)在握手阶段建立加密通道,一旦握手成功,后续所有 MQTT 报文(包括 CONNECT、PUBLISH、SUBSCRIBE、PINGREQ、DISCONNECT 等)都会通过这个加密通道传输

源码(netty-sample-05-tls)

https://gitee.com/kcnf-iot/iot-sample/tree/master/netty/netty-sample-05

混淆

  • "TLS" 和 "SSL" 混用

SSL(Secure Sockets Layer) 是 TLS 的前身,已弃用(SSL 2.0/3.0 均有严重漏洞)
TLS 1.0、1.1、1.2、1.3 是现行标准。很多人仍习惯说 "SSL",但实际指的都是 TLS。推荐只说 TLS

  • "启用 TLS" 不等于 "客户端身份认证"

启用 TLS(单向)时,只有服务端提供证书,客户端验证服务端身份。客户端可以是任何实体,不需要任何凭证
客户端身份认证 需要开启 双向 TLS(mTLS),即服务端也要求客户端提供证书。这才是"使用 X.509 证书认证"的常见含义

  • "证书" 和 "密钥" 混淆

    证书:公钥 + 元数据 + CA 签名,是公开的,可以被任何人获取
    私钥:必须保密,用于解密或签名。私钥丢失等同于身份被盗
    常见错误:把证书文件(.crt)和私钥文件(.key)弄反,或者误以为证书中包含私钥

  • "自签名证书" vs "CA 签名证书"

    自签名证书:自己给自己签发,没有第三方 CA 背书。浏览器会报"不安全",适合内部测试。
    CA 签名证书:由受信任的 CA 签发,生产环境必须使用。免费 CA(如 Let's Encrypt)或商业 CA
    混淆点:认为自签名证书"不安全"是因为加密强度不够------实际上加密强度相同,问题在于无法验证身份的真实性,容易遭受中间人攻击

  • "TLS 握手" 和 "应用层认证" 混淆

    TLS 握手发生在 TCP 连接之后、应用层数据(如 HTTP、MQTT)之前
    握手完成后,应用层可以再进行自己的认证(如 HTTP Basic Auth、MQTT 用户名/密码)
    混淆:以为 TLS 证书认证可以替代应用层认证。实际上它们是互补的,TLS 保证通道安全,应用层决定具体身份

认证只在连接环节完成

MQTT 协议规范中,身份验证(Authentication)是在 CONNECT 报文 处理时一次性完成

复制代码
客户端发送 CONNECT 报文(其中可携带用户名/密码、JWT 等凭据)
Broker 验证凭据有效性
验证通过则回复 CONNACK(成功),并记录该连接为"已认证"状态
此后,该连接上的任何后续报文(订阅、发布等)不再重复进行身份认证,而是基于已认证的身份执行授权检查(ACL)

自己生成 jks 证书

  • 找到jdk安装bin目录

  • 执行如下命令

    ./keytool.exe -genkeypair -alias server -keyalg RSA -keysize 2048 -validity 365 -keystore server-keystore.jks -storepass password -keypass password -dname "CN=localhost, OU=MyOrgUnit, O=MyOrg, L=Shanghai, ST=Shanghai, C=CN"

  • 默认生成之后的文件bin目录下面

源码演示

服务端代码
复制代码
package com.jysemel.iot;

import com.jysemel.iot.handler.MqttTlsHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.mqtt.MqttDecoder;
import io.netty.handler.codec.mqtt.MqttEncoder;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import lombok.SneakyThrows;

import javax.net.ssl.KeyManagerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;

public class MqttTlsServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ChannelPipeline p = ch.pipeline();
                            // 启动 TLS 加密服务 (8883) -- 需要提供 keystore
                            SslContext sslContext = createSslContext();
                            p.addLast("ssl", sslContext.newHandler(ch.alloc()));
                            // (A) 使用 Netty 内置的 MqttDecoder
                            p.addLast("mqttDecoder", new MqttDecoder(1024));
                            // (B) 使用单例的 MqttEncoder
                            p.addLast("mqttEncoder", MqttEncoder.INSTANCE);
                            // (C) 实现我们自己的业务逻辑处理器
                            p.addLast("authHandler", new MqttTlsHandler());
                        }
                    });
            ChannelFuture f = b.bind(9883).sync();
            System.out.println("MQTT tls Server started on port 9883");
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }



    @SneakyThrows
    private static SslContext createSslContext()  {
        String keyStorePath = "server-keystore.jks";
        String keyStorePass = "password";

        // 方式1:通过当前线程的上下文类加载器(最可靠)
        InputStream keyStoreStream = Thread.currentThread()
                .getContextClassLoader()
                .getResourceAsStream(keyStorePath);

        // 方式2:如果方式1失败,尝试直接使用文件系统(测试用)
        if (keyStoreStream == null) {
            File file = new File("src/main/resources/" + keyStorePath);
            if (file.exists()) {
                keyStoreStream = new FileInputStream(file);
                System.out.println("使用文件系统路径加载:" + file.getAbsolutePath());
            }
        }

        if (keyStoreStream == null) {
            throw new RuntimeException("无法加载证书文件: " + keyStorePath + "。请确保文件位于 resources 目录下,且已编译到 target/classes。");
        }

        KeyStore keyStore = KeyStore.getInstance("JKS");
        keyStore.load(keyStoreStream, keyStorePass.toCharArray());
        keyStoreStream.close();

        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(keyStore, keyStorePass.toCharArray());

        System.out.println("成功加载 TLS 证书");
        return SslContextBuilder.forServer(kmf).build();
    }
}
客户端 代码
复制代码
package com.jysemel.iot;

import com.jysemel.iot.handler.MqttTlsClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.mqtt.MqttDecoder;
import io.netty.handler.codec.mqtt.MqttEncoder;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;

import javax.net.ssl.SSLException;

public class MqttTlsClient {
    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            // 创建 SSLContext(测试环境信任所有证书,生产环境需配置 TrustStore)
            SslContext sslContext = createSslContext();

            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ChannelPipeline p = ch.pipeline();
                            // 添加 SslHandler(必须在编解码器之前)
                            p.addLast("ssl", sslContext.newHandler(ch.alloc(), "127.0.0.1", 9883));
                            p.addLast("mqttDecoder", new MqttDecoder(1024));
                            p.addLast("mqttEncoder", MqttEncoder.INSTANCE);
                            p.addLast("clientHandler", new MqttTlsClientHandler());
                        }
                    });
            ChannelFuture f = b.connect("127.0.0.1", 9883).sync();
            System.out.println("MQTT Client connected to TLS port 9883");
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }

    private static SslContext createSslContext() throws SSLException {
        // 测试环境:信任所有服务端证书(不验证证书合法性,仅用于快速测试)
        // 生产环境绝不能这样使用!
        return SslContextBuilder.forClient()
                .trustManager(InsecureTrustManagerFactory.INSTANCE)
                .build();
    }
}
演示结果
相关推荐
Halo_tjn1 小时前
NIO 技术的使用
java·开发语言·nio
SEO_juper1 小时前
JavaScript 渲染:AI 智能体无法读取,直接影响收录
开发语言·前端·javascript·aigc·seo·跨境电商·geo
Python+991 小时前
C++ 内存模型 & 底层原理
java·jvm·c++
jllllyuz1 小时前
通信信号调制识别系统(MATLAB实现)
开发语言·matlab
兰令水1 小时前
2026.5.30休息一天
java
公众号-老炮说Java1 小时前
Spring AI Alibaba 硬核实战:Token 原理 → RAG → 多智能体,一篇通
java·人工智能·后端·spring
Kurisu5751 小时前
深度解析:Java 对象的内存布局与指针压缩原理
java·开发语言
garmin Chen1 小时前
Elasticsearch(2):JavaRestClient操作Elasticsearch全流程实战指南
java·大数据·elasticsearch·搜索引擎
zoyation1 小时前
Spring Boot多数据源
java·spring boot·后端