# 背景消息

设备证书是由CA根证书签发给客户端设备使用的数字证书,用于客户端和服务端连接时,服务端对客户端进行安全认证。认证通过后服务端和客户端可基于证书内的加密密钥进行安全通信,若认证不通过则服务端拒绝客户端接入。 使用设备证书认证时,必须保证签发该设备证书的CA证书已在MQTT服务端中注册。 客户端设备使用设备证书进行接入认证时,服务端会根据已注册的CA证书验证设备证书是否正确,若CA证书和设备证书匹配成功,则客户端认证通过,且系统会将该设备证书自动注册到服务端中。

# 双向SSL/TLS安全连接

作为基于现代密码学公钥算法的安全协议,TLS/SSL 能在计算机通讯网络上保证传输安全,很多MQTT Broker 内置对 TLS/SSL 的支持,包括支持单/双向认证、X.509 证书、负载均衡 SSL 等多种安全认证。

# SSL/TLS 安全优势

强认证: 用 TLS 建立连接的时候,通讯双方可以互相检查对方的身份。在实践中,很常见的一种身份检查方式是检查对方持有的 X.509 数字证书。这样的数字证书通常是由一个受信机构颁发的,不可伪造。

保证机密性: TLS 通讯的每次会话都会由会话密钥加密,会话密钥由通讯双方协商产生。任何第三方都无法知晓通讯内容。即使一次会话的密钥泄露,并不影响其他会话的安全性。

完整性: 加密通讯中的数据很难被篡改而不被发现。

# SSL/TLS 协议

TLS/SSL 协议下的通讯过程分为两部分,第一部分是握手协议。握手协议的目的是鉴别对方身份并建立一个安全的通讯通道。握手完成之后双方会协商出接下来使用的密码套件和会话密钥;第二部分是 record 协议,record 和其他数据传输协议非常类似,会携带内容类型,版本,长度和荷载等信息,不同的是它所携带的信息是加密了的。 下面的图片描述了 TLS/SSL 握手协议的过程,从客户端的 “hello” 一直到服务器的 “finished” 完成握手。有兴趣的同学可以找更详细的资料看。

image-20230412154939444

# SSL/TLS 证书准备

在双向认证中,一般都使用自签名证书的方式来生成服务端和客户端证书,因此本文就以自签名证书为例。

通常来说,我们需要数字证书来保证 TLS 通讯的强认证。数字证书的使用本身是一个三方协议,除了通讯双方,还有一个颁发证书的受信第三方,有时候这个受信第三方就是一个 CA。和 CA 的通讯,一般是以预先发行证书的方式进行的。也就是在开始 TLS 通讯的时候,我们需要至少有 2 个证书,一个 CA 的,一个 MQTT服务端 的, MQTT服务端的证书由 CA 颁发,并用 CA 的证书验证。 在这里,我们假设您的系统已经安装了 OpenSSL。使用 OpenSSL 附带的工具集就可以生成我们需要的证书了。

已安装OpenSSL v1.1.1i或以上版本。

使用OpenSSL创建生成CA证书、服务器、客户端证书及密钥

  1. 生成CA证书
  2. 生成服务器证书
  3. 生成客户端证书
  • 对于SSL单向认证:服务器需要CA证书、server证书、server私钥,客户端需要CA证。
  • 对于SSL双向认证:服务器需要CA证书、server证书、server私钥,客户端需要CA证书,client证书、client私钥。

# 各类证书与密钥文件后缀的解释

总得来说这些文件都与X.509证书和密钥文件有关,从文件编码上分,

只有两大类: PEM格式:使用Base64 ASCII进行编码的纯文本格式 DER格式:二机制格式

而CRT, CER,KEY这几种证书和密钥文件,它们都有自己的schema,在存储为物理文件时,既可以是PEM格式,也可以DER格式。 CER:一般用于windows的证书文件格式 CRT:一般用于Linux的证书,包含公钥和主体信息 KEY:一般用于密钥,特别是私钥, 与证书一一配对 打个比方:CER,CRT,KEY相当于论文,说明书等,有规定好的行文格式与规范,而PEM和DER相当于txt格式还是word格式。

CSR: Certificate Signing Request,即证书签名请求文件。证书申请者在生成私钥的同时也生成证书请求文件。把CSR文件提交给证书颁发机构后,证书颁发机构使用其根证书私钥签名就生成了证书公钥文件,也就是颁发给用户的证书。

# 一、证书生成-服务器(基于openssl)

# 1.1.生成CA证书

# 1.1.1 创建CA证书私钥(key)
openssl genrsa -out ca.key 2048

image-20230412155618525

# 1.1.2. 请求证书(csr)
openssl req -new -sha256 -key ca.key -out ca.csr -subj "/C=CN/ST=SZ/L=SZ/O=C.X.L/OU=C.X.L/CN=CA/emailAddress=123456@test.com"

image-20230412155650323

证数各参数含义如下 C-----国家(Country Name) ST----省份(State or Province Name) L----城市(Locality Name) O----公司(Organization Name) OU----部门(Organizational Unit Name) CN----产品名(Common Name) emailAddress----邮箱(Email Address)

# 1.1.3. 自签署证书(crt)
openssl x509 -req -days 36500 -sha256 -extensions v3_ca -signkey ca.key -in ca.csr -out ca.crt

image-20230412155953809

生成后的文件

image-20230412160302917

# 1.2. 生成服务端证书

# 1.2.1.创建服务器私钥(key)
openssl genrsa -out server.key 2048

image-20230412160604779

新建 openssl.cnf 文件

vim openssl.cnf
  • req_distinguished_name :根据情况进行修改,
  • alt_names:BROKER_ADDRESS 修改为 EMQ X 服务器实际的 IP 或 DNS 地址,例如:IP.1 = 127.0.0.1,或 DNS.1 = broker.xxx.com
  • 注意:IP 和 DNS 二者保留其一即可,如果已购买域名,只需保留 DNS 并修改为你所使用的域名地址。
[req]
default_bits       = 2048
distinguished_name = req_distinguished_name
req_extensions     = req_ext
x509_extensions    = v3_req
prompt             = no

[req_distinguished_name]
countryName             = CN
stateOrProvinceName     = SZ
organizationName        = C.X.L
organizationalUnitName  = C.X.L
commonName          = service

[req_ext]
subjectAltName = @alt_names

[v3_req]
subjectAltName = @alt_names

[alt_names]
IP.1 = 127.0.0.1
# 1.2.2. 请求证书(csr)
openssl req -new -sha256 -key server.key -config openssl.cnf -out server.csr

image-20230412161620830

# 1.2.3. 使用CA证书签署服务器证书(crt)
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650 -sha256 -extensions v3_req -extfile openssl.cnf

image-20230412162146793

# 1.2.4. 验证服务端证书
openssl verify -CAfile ca.crt server.crt

image-20230412162245419

# 1.2.5. 查看服务端证书
openssl x509 -noout -text -in server.crt
image-20230412162347120
# 1.2.6. Netty需要支持PKCS8格式读取私钥
openssl pkcs8 -topk8 -nocrypt -in server.key -out pkcs8_key.pem

image-20230412162555345

# 1.3. 生成客户端证书

# 1.3.1. 生成客户端私钥(key)
openssl genrsa -out client.key 2048
image-20230412162809182
# 1.3.2. 请求证书(csr)
openssl req -new -sha256 -key client.key -out client.csr -subj "/C=CN/ST=SZ/L=SZ/O=C.X.L/OU=C.X.L/CN=CLIENT/emailAddress=123456@test.com"

image-20230412163025195

# 1.3.3. 使用CA证书签署客户端证书(crt)
openssl x509 -req -days 36500 -sha256 -extensions v3_req -CA ca.crt -CAkey ca.key -CAserial ca.srl -CAcreateserial -in client.csr -out client.crt

image-20230412163245338

# 1.3.4. 验证服务端证书
openssl verify -CAfile ca.crt client.crt

image-20230412163351371

# 1.3. 5. 查看服务端证书
openssl x509 -noout -text -in client.crt
image-20230412163432162

生成后的文件

image-20230412163749986

# 1.4. 证书转换

既然PEM与DER只是编码格式上的不同,那么不管是证书还是密钥,都可以随意转换为想要的格式:

# 1.4.1. CRT转为PEM
#.key 转换成 .pem:
openssl rsa -in server.key -out server-key.pem
#.crt 转换成 .pem:
openssl x509 -in server.crt -out server.pem -outform PEM

image-20230412163935991

# 1.4.2. PEM转DER
#.pem 转换成 .der:
openssl x509 -inform pem -outform der -in server.pem -out server.der

image-20230412164858867

# 1.4.3. DER转PEM
openssl x509 -inform der -outform pem -in server.der -out server.pem

image-20230412164911253

# 1.4.4. 直接生成PEM格式的证书
// 生成CA证书
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.pem -subj "/C=CN/ST=SZ/L=SZ/O=C.X.L/OU=C.X.L/CN=CA/emailAddress=123456@test.com"

// 生成服务端证书
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server.pem -days 3650 -sha256 -extensions v3_req -extfile openssl.cnf

// 生成客户端证书
openssl x509 -req -days 3650 -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out client.pem

image-20230412165430280

# 二、证书生成-本机(基于keytool)

java生成HTTPS证书:

既然是双向验证,就需要双方的密钥,我们服务端称为localhost,而客户端称为client。需要生成双方的密钥文件,并把对方的cert导入自己的密钥文件里。整个过程如下: 注意:密码统一为:changeit,这个密码自己可以设置,然后记住就可以了

cer格式 :用于存储公钥证书的文件格式

jks格式:JKS文件是一种经过加密的安全文件,以二进制的Java密钥库(KeyStore)格式存储的一组密钥和证书,需要密码才能打开

# 2.1. 生成服务端证书

#生成服务端密钥文件localhost.jks
keytool -genkey -alias localhost -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore localhost.jks -dname CN=localhost,OU=Test,O=pkslow,L=Guangzhou,C=CN -validity 731 -storepass changeit -keypass changeit

#导出服务端的localhost.cer文件
keytool -export -alias localhost -file localhost.cer -keystore localhost.jks

image-20230412194610121

命令行重要参数的意义:

  • alias:密钥别名,可以随便起,不冲突就行;

  • keyalg:加密算法;

  • keysize:密钥长度,2048基本就不可能破解了;

  • keystore:keystore的文件名;

  • dname:这个很关键,特别是CN=后面要按正确的域名来写;

  • validity:cert的有效期

# 2.2. 生成客户端证书

#生成客户端的密钥文件client.jks
keytool -genkey -alias client -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore client.jks -dname CN=client,OU=Test,O=pkslow,L=Guangzhou,C=CN -validity 731 -storepass changeit -keypass changeit

#导出客户端的client.cer文件
keytool -export -alias client -file client.cer -keystore client.jks

image-20230412194842091

# 2.3. 服务端和客户端相互导入cer文件

#把客户端的cer导入到服务端
keytool -import -alias client -file client.cer -keystore localhost.jks

image-20230412195352486

#把服务端的cer导入到客户端
keytool -import -alias localhost -file localhost.cer -keystore client.jks

image-20230412195528568

#检验服务端是否具有自己的private key和客户端的cer
keytool -list -keystore localhost.jks

image-20230412200343905

#检验客户端是否具有自己的private key和服务端的cer
keytool -list -keystore client.jks 

image-20230412200308774

# 2.4. 转换JKS格式为P12

为了建立连接,应该要把客户端的密钥文件给谷歌浏览器使用。因为JKS是Java的密钥文件格式,我们转换成通用的PKCS12格式如下:

#转换JKS格式为P12
keytool -importkeystore -srckeystore client.jks -destkeystore client.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass changeit -deststorepass changeit -srckeypass changeit -destkeypass changeit -srcalias client -destalias client -noprompt

image-20230412200604969

成功执行完上述步骤后,会生成5个文件:

  • 服务端密钥文件: localhost.jks
  • 服务端cert文件:localhost.cer
  • 客户端密钥文件:client.jks
  • 客户端cert文件:client.cer
  • 客户端能识别的密钥文件:client.p12

image-20230412200706853

image-20230412200718033

# 2.5. 配置SpringBoot

# 2.5.1. 修改配置文件

在application.properties或者application.yml中去配置即可:

server:
    port: 8443
    ssl:
        client-auth: need
        enabled: true
        key-alias: localhost
        key-store: classpath:localhost.jks
        key-store-password: changeit
        key-store-type: JKS
        trust-store: classpath:localhost.jks
        trust-store-password: changeit
        trust-store-provider: SUN
        trust-store-type: JKS

需要将生成的localhost.jks文件放到springboot项目中的resource文件夹下,然后重启项目,就可以了。 需要分别配置Key StoreTrust Store的文件、密码等信息,即使是同一个文件。 需要注意的是,server.ssl.client-auth有三个可配置的值:none、wantneed双向验证应该配置为neednone表示不验证客户端;want表示会验证,但不强制验证,即验证失败也可以成功建立连接。

# 2.5.2. 开启多个Connector

如果只配置了上面的信息,就相当于开启了https,禁用了http。

如果还需要支持http,需要通过代码实现。官方文档在[这儿]https://docs.spring.io/spring-boot/docs/2.4.0/reference/html/howto.html#howto-enable-multiple-connectors-in-tomcat)说明了如何开启多个Connector。

官方提供的示例代码如下:

@Bean
public ServletWebServerFactory servletContainer() {
    TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
    tomcat.addAdditionalTomcatConnectors(createSslConnector());
    return tomcat;
}

private Connector createSslConnector() {
    Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
    Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
    try {
        File keystore = new ClassPathResource("keystore").getFile();
        File truststore = new ClassPathResource("keystore").getFile();
        connector.setScheme("https");
        connector.setSecure(true);
        connector.setPort(8443);
        protocol.setSSLEnabled(true);
        protocol.setKeystoreFile(keystore.getAbsolutePath());
        protocol.setKeystorePass("changeit");
        protocol.setTruststoreFile(truststore.getAbsolutePath());
        protocol.setTruststorePass("changeit");
        protocol.setKeyAlias("apitester");
        return connector;
    }
    catch (IOException ex) {
        throw new IllegalStateException("can't access keystore: [" + keystore
                + "] or truststore: [" + truststore + "]", ex);
    }
}

上面是官方提供的示例代码,它开启了一个SSL Connector。我们要在默认开启https的基础上至此http访问。应该对上面的代码进行改造。在启动类中添加如下代码

@Value("${server.http.port}")
    private int httpPort;

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        tomcat.addAdditionalTomcatConnectors(createHttpConnector());
        return tomcat;
    }

    private Connector createHttpConnector() {
        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        connector.setSecure(false);
        connector.setPort(httpPort);
        return connector;
    }

在application.yml中添加配置项: server.http.port=8080

server:
    port: 8443
    ssl:
        client-auth: need
        enabled: true
        key-alias: localhost
        key-store: classpath:localhost.jks
        key-store-password: changeit
        key-store-type: JKS
        trust-store: classpath:localhost.jks
        trust-store-password: changeit
        trust-store-provider: SUN
        trust-store-type: JKS
    http:
        port: 8080

启动应用, 发现有两个端口:https 8443端口、http8080端口;

image-20230413115947166

# 2.5.3. 编写Controller
@RestController
@RequestMapping("/api")
public class SysTestController {

    @GetMapping("/test")
    public String test() {
        return "双向认证";
    }

# 2.6. 用Postman测试双向验证

完成密钥文件准备和配置后,启动SpringBoot便可以了,使用Postman测试。

# 2.6.1. 新建一个请求

image-20230412213224374

# 2.6.2. 添加证书

找到右上角的Settings->Certificates->Client Certificates->Add Certificate选择文件,输入密码添加即可。

image-20230413141601389
# 2.6.2. 测试请求

HTTPS接口请求成功,输出"双向认证"

image-20230413141628473

# 2.7. http转发到https

http的redirectPort只有在所使用的接口一定要使用https访问的时候才会跳转。所以还要再添加一些代码,指定默认Https Connector的拦截url。修改前面的servletContainer方法

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory(){
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint securityConstraint = new SecurityConstraint();
                securityConstraint.setUserConstraint("CONFIDENTIAL");
                SecurityCollection collection = new SecurityCollection();
                //对/api/下的所有接口进行转发
                collection.addPattern("/api/*");
                securityConstraint.addCollection(collection);
                context.addConstraint(securityConstraint);
            }
        };
        tomcat.addAdditionalTomcatConnectors(createHttpConnector());
        return tomcat;
    }

最终的启动类

    public static void main(String[] args) {
        SpringApplication.run(CodeGeneratorApplication.class, args);
    }

    @Value("${server.http.port}")
    private int httpPort;

    @Value("${server.port}")
    private int httpsPort;

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory(){
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint securityConstraint = new SecurityConstraint();
                securityConstraint.setUserConstraint("CONFIDENTIAL");
                SecurityCollection collection = new SecurityCollection();
                //对/api/下的所有接口进行转发
                collection.addPattern("/api/*");
                securityConstraint.addCollection(collection);
                context.addConstraint(securityConstraint);
            }
        };
        tomcat.addAdditionalTomcatConnectors(createHttpConnector());
        return tomcat;
    }

    private Connector createHttpConnector() {
        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        connector.setSecure(false);
        connector.setPort(httpPort);
        connector.setRedirectPort(httpsPort);
        return connector;
    }

测试

在浏览器输入http://localhost:8080/api/test会跳转到 https://localhost:8443/api/test

image-20230413143014705

mac解决自签名证书无法访问的问题

在钥匙串访问中导入client.p12证书,按照提示操作

image-20230413143516171

设置始终信任,点击证书->显示简介->信任,选择始终信任。

image-20230413143722619

mac下的chrome对自签证书无法访问,默认点高级也没有继续不安全访问选项。

经过查资料发现chrome留了一个小路。

就是在将要访问的页面,点一下任意位置然后输入 thisisunsafe 就可以访问了,按照提示操作即可

image-20230413144030080