emqx配置ssl/tsl实现双向认证

Posted luffy5459

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了emqx配置ssl/tsl实现双向认证相关的知识,希望对你有一定的参考价值。

    emqx是一种基于mqtt协议实现的消息框架,目前很多地方已经开始使用。简单的emqx安装配置,其实就可以作为一个broker来使用,客户端只需要通过ip、端口、用户名、密码、clientid就可以连接了,至于发送消息,直接在发送的时候指定topic即可。

    简单的emqx安装配置,使用的协议是:mqtt:tcp,使用的url是tcp://ip:1883。这种方式,其实可以很容易被模拟(理论上的可能),所以有了mqtts:tls协议,一般使用ssl协议来实现。这也是本文所要阐述的问题。

    我在配置ssl证书的时候,服务启动没问题,最后客户端连接,死活连接失败。最后看了官方的文档,按照那个文档很容易就配置成功,并且客户端连接也成功了。

    其实我大概知道我配置失败的原因,可能是因为证书中的一个跟主机ip相关的设置问题,但是这个问题我按照一些博客的提示做了修改,但是后来还是没有成功,很郁闷,最后按照官方配置,很快设置好了。思路基本一模一样,而且配置证书的步骤也都类似,先生成私钥,然后生成证书请求,最后根据根证书生成服务端证书和客户端证书。

    emqx的下载安装这里不做过多的说明,在官网直接下载,解压然后运行bin/emqx start就可以了。

   emqx安装目录下有一个etc/certs目录,它自带了一些根证书、服务端证书、客户端证书,但是为了保险起见,最好自己根据自己的主机地址生成。

   下面基本是按照emqx官方的一篇博客来配置的,就是主机地址做了修改。

    以下的证书配置,是自签名证书,不是花钱购买的那种服务器证书。

    1、生成根证书私钥

openssl genrsa -out ca.key 2048

    2、生成根证书

openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.pem

    这一步会有交互提示,需要输入省份、城市、公司、组织、姓名、右键,随便填写就可以,这里的信息不会影响根证书使用。 

    3、生成服务端证书私钥

openssl genrsa -out emqx.key 2048

     4、生成服务端证书请求

    按照官方博客的意思,这里需要手动创建一个openssl.cnf的文件,其实主要就是设置IP地址

[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 = Hubei
localityName = Wuhan
organizationName = EMQX
commonName = CA
[req_ext]
subjectAltName = @alt_names
[v3_req]
subjectAltName = @alt_names
[alt_names]
IP.1 = 192.168.226.100
DNS.1 = 192.168.226.100

    如果你的服务器是虚拟机,只需要修改IP.1的值即可。

    执行如下命令生成服务端证书请求:

openssl req -new -key ./emqx.key -config openssl.cnf -out emqx.csr

    私钥的生成,就使用了openssl.cnf配置文件 ,所以我之前的配置失败,应该是这里的IP设置问题。但是我确实根据某些博客的提示,设置subj "/CN=192.168.226.100",但是不生效。

     5、生成服务端证书

openssl x509 -req -in ./emqx.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out emqx.pem -days 3650 -sha256 -extensions v3_req -extfile openssl.cnf

     6、生成客户端证书私钥

openssl genrsa -out client.key 2048

     7、生成客户端证书请求

openssl req -new -key client.key -out client.csr -subj "/C=CN/ST=Zhejiang/L=Hangzhou/O=EMQX/CN=client"

      8、生成客户端证书

openssl x509 -req -days 3650 -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out client.pem

一顿操作之后certs目录下生成的文件:

 

以上,只是生成了相关证书,在emqx的配置文件etc/emqx.conf中还需要做相关设置:

listener.ssl.external.keyfile = etc/certs/emqx.key
listener.ssl.external.certfile = etc/certs/emqx.pem
listener.ssl.external.cacertfile = etc/certs/ca.pem
listener.ssl.external.verify = verify_peer
listener.ssl.external.fail_if_no_peer_cert = true

    这几个主要的配置在原来的emqx.conf中都有,只需要打开或者修改配置值即可,主要是证书的名字。

    重启emqx服务,验证。bin/emqx restart

     这里使用mqttbox来验证,其实mqtt.x也是可以的,客户端需要的文件:ca.pem,client.pem,client.key 

     普通的mqtt/tcp协议连接配置:

    mqtts/tls协议连接配置:

 

    配置需要注意的地方在上图中已经标识出来了,协议类型:mqtts/tls ,证书类型:自签名证书,ca文件:ca.pem,客户端证书:client.pem,客户端私钥:client.key。

     配置完成,保存,可以看到连接状态变为绿色,如果是红色,那么就需要检查证书是否配置正确,包括生成和在emqx.conf配置文件中的设置。

     发送消息测试:

  下面通过java代码来验证,这里使用springboot+spring-integration-mqtt来实现。

  maven工程的pom.xml

<dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-integration</artifactId>
    </dependency>
    <dependency>
    	<groupId>org.springframework.integration</groupId>
    	<artifactId>spring-integration-mqtt</artifactId>
    </dependency>
    <dependency>
    	<groupId>org.eclipse.paho</groupId>
    	<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
    	<version>1.2.5</version>
    </dependency>
    
    <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcpkix-jdk15on</artifactId>
            <version>1.47</version>
     </dependency>

    application-dev.yml

mqtt:
  serverURIs: ssl://192.168.226.100:8883
  username: admin
  password: public
  client:
    id: $random.value
  topic: test

     MqttConfig.java

package com.huali.mec.receiver.config;
import java.util.Objects;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;

import com.huali.mec.receiver.util.SslUtil;
@IntegrationComponentScan
@Configuration
public class MqttConfig 
	public static final Logger log  = LoggerFactory.getLogger(MqttConfig.class);
	
	public static final String OUTPUT_CHANNEL = "mqttOutputChannel";
	public static final String INPUT_CHANNEL = "mqttInputChannel";
	
	@Value("$mqtt.username")
	private String username ;
	@Value("$mqtt.password")
	private String password ;
	@Value("$mqtt.serverURIs")
	private String serverURIs ;
	@Value("$mqtt.client.id")
	private String clientId;
	@Value("$mqtt.topic")
	private String defaultTopic ;
	
	@Bean
	public MqttPahoClientFactory clientFactory() 
		final MqttConnectOptions options = new MqttConnectOptions();
		options.setServerURIs(new String[] serverURIs);
		options.setUserName(username);
		options.setPassword(password.toCharArray());
		options.setKeepAliveInterval(10);
		options.setAutomaticReconnect(true);
		try 
			options.setSocketFactory(SslUtil.getSocketFactory("src/main/resources/ssl/ca.pem", "src/main/resources/ssl/client.pem", "src/main/resources/ssl/client.key", password));
		 catch (Exception e) 
			e.printStackTrace();
		
		final DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
		factory.setConnectionOptions(options);
		return factory;
	
	
	@Bean(value = OUTPUT_CHANNEL)
	public MessageChannel mqttOutChannel() 
		return new DirectChannel();
	
	
	@Bean
	@ServiceActivator(inputChannel = OUTPUT_CHANNEL)
	public MessageHandler mqttOutbound() 
		final MqttPahoMessageHandler handler = new MqttPahoMessageHandler(clientId, clientFactory());
		handler.setDefaultQos(1);
		handler.setDefaultRetained(false);
		handler.setDefaultTopic(defaultTopic);
		handler.setAsync(false);
		handler.setAsyncEvents(false);
		return handler;
	
	
	
	@Bean
	public MessageChannel mqttInputChannel() 
		return new DirectChannel();
	
	
	@Bean
	public MessageProducer inbound() 
		MqttPahoMessageDrivenChannelAdapter adapter = 
				new MqttPahoMessageDrivenChannelAdapter(
				clientId+"_inbound", clientFactory(), defaultTopic);
		adapter.setCompletionTimeout(3000);
		adapter.setConverter(new DefaultPahoMessageConverter());
		adapter.setQos(1);
		adapter.setOutputChannel(mqttInputChannel());
		return adapter;
	
	
	@Autowired
	private ApplicationEventPublisher eventPublisher ;
	
	@Bean
	@ServiceActivator(inputChannel = INPUT_CHANNEL)
	public MessageHandler handler() 
		return message -> 
			String topic = Objects.requireNonNull(message.getHeaders().get("mqtt_receivedTopic")).toString();
			log.info("topic : ,payload :",topic,message.getPayload().toString());
			//eventPublisher.publishEvent(new MqttEvent(this, topic, message.getPayload().toString()));
		;
	
	

    SslUtil.java,读取客户端证书并生成SSLSocketFactory给MQTT连接使用

package com.huali.mec.receiver.util;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.X509Certificate;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.openssl.PasswordFinder;

public class SslUtil 
	public static SSLSocketFactory getSocketFactory(final String caCrtFile,final String clientCrtFile,
			final String keyFile,final String password) throws Exception 
		Security.addProvider(new BouncyCastleProvider());
		//load ca certificate
		PEMReader reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(caCrtFile)))));
		X509Certificate caCert = (X509Certificate)reader.readObject();
		reader.close();
		
		//load client certificate
		reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(clientCrtFile)))));
		X509Certificate clientCert = (X509Certificate)reader.readObject();
		reader.close();
		//load client key
		reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(keyFile)))),
				new PasswordFinder() 
					
					@Override
					public char[] getPassword() 
						return password.toCharArray();
					
		);
		
		KeyPair key = (KeyPair)reader.readObject();
		
		reader.close();
		
		KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
		
		keyStore.load(null, null);
		keyStore.setCertificateEntry("ca-certificate", caCert);
		TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
		tmf.init(keyStore);
		
		KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
		ks.load(null, null);
		ks.setCertificateEntry("certificate", clientCert);
		ks.setKeyEntry("private-key", key.getPrivate(), password.toCharArray(),new java.security.cert.Certificate[] clientCert);
		KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
		kmf.init(ks,password.toCharArray());
		
		SSLContext context = SSLContext.getInstance("TLSv1.2");
		context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
		return context.getSocketFactory();
		
	

客户端使用的三个证书文件放到resources/ssl目录下,ca.pem,client.pem,client.key

 

 

     启动springboot项目,mqttbox客户端发送数据到test这个topic上,这边监听:

     控制台打印的消息:

2022-01-21 11:46:12.019  INFO 18368 --- [7785b5b_inbound] c.huali.mec.receiver.config.MqttConfig   : topic :test ,payload :"id":1,"name":"buejee"

以上是关于emqx配置ssl/tsl实现双向认证的主要内容,如果未能解决你的问题,请参考以下文章

计算机网络

EMQ功能使用(一) 实现MQTTS协议

HTTPS-SSL/TSL与SNI的关系以及同IP多域名虚拟主机的SSL/TSL认证

EMQ X 服务器 SSL/TLS 安全连接配置指南

加密之SSL和单双向认证

request发送HTTPS请求(处理SSL证书验证)