# 背景消息
设备证书是由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” 完成握手。有兴趣的同学可以找更详细的资料看。

# SSL/TLS 证书准备
在双向认证中,一般都使用自签名证书的方式来生成服务端和客户端证书,因此本文就以自签名证书为例。
通常来说,我们需要数字证书来保证 TLS 通讯的强认证。数字证书的使用本身是一个三方协议,除了通讯双方,还有一个颁发证书的受信第三方,有时候这个受信第三方就是一个 CA。和 CA 的通讯,一般是以预先发行证书的方式进行的。也就是在开始 TLS 通讯的时候,我们需要至少有 2 个证书,一个 CA 的,一个 MQTT服务端 的, MQTT服务端的证书由 CA 颁发,并用 CA 的证书验证。 在这里,我们假设您的系统已经安装了 OpenSSL。使用 OpenSSL 附带的工具集就可以生成我们需要的证书了。
已安装OpenSSL v1.1.1i或以上版本。
使用OpenSSL创建生成CA证书、服务器、客户端证书及密钥
- 生成CA证书
- 生成服务器证书
- 生成客户端证书
- 对于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

# 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"

证数各参数含义如下 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

生成后的文件

# 1.2. 生成服务端证书
# 1.2.1.创建服务器私钥(key)
openssl genrsa -out server.key 2048

新建 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

# 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

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

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

# 1.3. 生成客户端证书
# 1.3.1. 生成客户端私钥(key)
openssl genrsa -out client.key 2048
# 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"

# 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

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

# 1.3. 5. 查看服务端证书
openssl x509 -noout -text -in client.crt
生成后的文件

# 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

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

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

# 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

# 二、证书生成-本机(基于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

命令行重要参数的意义:
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

# 2.3. 服务端和客户端相互导入cer文件
#把客户端的cer导入到服务端
keytool -import -alias client -file client.cer -keystore localhost.jks

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

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

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

# 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

成功执行完上述步骤后,会生成5个文件:
- 服务端密钥文件: localhost.jks
- 服务端cert文件:localhost.cer
- 客户端密钥文件:client.jks
- 客户端cert文件:client.cer
- 客户端能识别的密钥文件:client.p12


# 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 Store和Trust Store的文件、密码等信息,即使是同一个文件。 需要注意的是,server.ssl.client-auth有三个可配置的值:none、want和need。双向验证应该配置为need;none表示不验证客户端;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端口;

# 2.5.3. 编写Controller
@RestController
@RequestMapping("/api")
public class SysTestController {
@GetMapping("/test")
public String test() {
return "双向认证";
}
# 2.6. 用Postman测试双向验证
完成密钥文件准备和配置后,启动SpringBoot便可以了,使用Postman测试。
# 2.6.1. 新建一个请求

# 2.6.2. 添加证书
找到右上角的Settings->Certificates->Client Certificates->Add Certificate选择文件,输入密码添加即可。
# 2.6.2. 测试请求
HTTPS接口请求成功,输出"双向认证"

# 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

mac解决自签名证书无法访问的问题
在钥匙串访问中导入client.p12证书,按照提示操作
设置始终信任,点击证书->显示简介->信任,选择始终信任。
mac下的chrome对自签证书无法访问,默认点高级也没有继续不安全访问选项。
经过查资料发现chrome留了一个小路。
就是在将要访问的页面,点一下任意位置然后输入 thisisunsafe 就可以访问了,按照提示操作即可
