activemq ssl java.security.cert.CertificateException: No name matching localhost found
解决activemq ssl java.security.cert.CertificateException: No name matching localhost found的问题
目录
Broker SSL Connector以及SSL证书的配置
问题描述:
在AMQ Broker配置ssl+nio的connector,安装SSL证书,客户端通过ssl进行topic消息的发送与消费时出现java.security.cert.CertificateException: No name matching localhost found的问题,下文是具体的过程
SSL证书的制作(未做CA签名)
#创建broker的keystore
keytool -genkey -alias broker -keyalg RSA -keystore broker.ks
#从broker keystore中导出证书
keytool -export -alias broker -keystore broker.ks -file broker_cert
#创建客户端的keystore
keytool -genkey -alias client -keyalg RSA -keystore client.ks
#将服务器端的证书导入客户端的keystore
keytool -import -alias broker -keystore client.ts -file broker_cert
注意:这里仅做单项认证,也就是说客户端认证服务器是否合法,并未做双向认证,双向认证可以参考文档:https://activemq.apache.org/how-do-i-use-ssl.html
Broker SSL Connector以及SSL证书的配置
将制作的证书配置在AMQ的Broker中,并且配置Connector使其生效
配置transportConnectors
<transportConnectors>
...
<transportConnector name="auto+nio+ssl" uri="auto+nio+ssl://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="nio+ssl" uri="nio+ssl://0.0.0.0:61617?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
...
</transportConnectors>
配置SSL证书
<sslContext>
<sslContext keyStore="conf/broker.ks"
keyStorePassword="******"
trustStore="conf/broker.ks"
trustStorePassword="******" />
</sslContext>
客户端程序关键代码
客户端代码比较简单(无论publish还是subscribe),所以我只贴出来关键部分的代码
//使用ActiveMQSslConnectionFactory,它是ActiveMQConnectionFactory的子类,做了一些关于SSLContext的声明
ActiveMQSslConnectionFactory factory = new ActiveMQSslConnectionFactory("ssl://192.168.88.3:61617");
factory.setKeyAndTrustManagers(getKeyManagers("keystore路径", "keystore密码"),
getTrustManagers(), new java.security.SecureRandom());
//...省略
private static TrustManager[] getTrustManagers()
throws NoSuchAlgorithmException, IOException,
KeyStoreException, CertificateException {
return new TrustManager[]{new X509TrustManager() {
private X509Certificate[] certificates;
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
if (x509Certificates == null) {
this.certificates = x509Certificates;
}
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
if (x509Certificates == null) {
this.certificates = x509Certificates;
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return certificates;
}
}};
}
private static KeyManager[] getKeyManagers(String keyStore, String keyStorePassword)
throws java.security.GeneralSecurityException, java.io.IOException {
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream(keyStore), keyStorePassword.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, keyStorePassword.toCharArray());
return kmf.getKeyManagers();
}
出现SSL问题,排查思路
重点来了,运行上面的程序会出现“Caused by: javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No name matching localhost found”,主要原因是我们生成的证书没有经过CA认证并且和FQDN进行关联,客户端并不认为当前所使用的证书可以认证所连接的broker是安全可信的,我们也可以通过openssl命令创建ca证书,然后再使用ca对证书签名。但实际上在AMQ中有另外一种做法更加的方便--那就是忽略对broker主机的校验,这样我们将无需对证书进行签名操作了。
阅读源代码了解AMQ中Secure通信的方式
ActiveMQSslConnectionFactory
//关键代码部分:主要使用创建SslContext,并调用父类的createTransport方法
@Override
protected Transport createTransport() throws JMSException {
SslContext existing = SslContext.getCurrentSslContext();
try {
if (keyStore != null || trustStore != null) {
keyManager = createKeyManager();
trustManager = createTrustManager();
}
if (keyManager != null || trustManager != null) {
SslContext.setCurrentSslContext(new SslContext(keyManager, trustManager, secureRandom));
}
return super.createTransport();
} catch (Exception e) {
throw JMSExceptionSupport.create("Could not create Transport. Reason: " + e, e);
} finally {
SslContext.setCurrentSslContext(existing);
}
}
ActiveMQConnectionFactory
由于AMQ支持很多的协议,因此Transport的创建交给了不同的TransportFactory来创建不同写一下的Transport
protected Transport createTransport() throws JMSException {
try {
URI connectBrokerUL = brokerURL;
//获取schema,其实就是获取不同的协议
String scheme = brokerURL.getScheme();
if (scheme == null) {
throw new IOException("Transport not scheme specified: [" + brokerURL + "]");
}
if (scheme.equals("auto")) {
connectBrokerUL = new URI(brokerURL.toString().replace("auto", "tcp"));
} else if (scheme.equals("auto+ssl")) {
connectBrokerUL = new URI(brokerURL.toString().replace("auto+ssl", "ssl"));
} else if (scheme.equals("auto+nio")) {
connectBrokerUL = new URI(brokerURL.toString().replace("auto+nio", "nio"));
} else if (scheme.equals("auto+nio+ssl")) {
connectBrokerUL = new URI(brokerURL.toString().replace("auto+nio+ssl", "nio+ssl"));
}
//TransportFactory连接
return TransportFactory.connect(connectBrokerUL);
} catch (Exception e) {
throw JMSExceptionSupport.create("Could not create Transport. Reason: " + e, e);
}
}
TransportFactory
public static Transport connect(URI location) throws Exception {
TransportFactory tf = findTransportFactory(location);
return tf.doConnect(location);
}
...
public static TransportFactory findTransportFactory(URI location) throws IOException {
String scheme = location.getScheme();
if (scheme == null) {
throw new IOException("Transport not scheme specified: [" + location + "]");
}
//根据schema获取不同的TransportFactory实例
TransportFactory tf = TRANSPORT_FACTORYS.get(scheme);
if (tf == null) {
// Try to load if from a META-INF property.
try {
tf = (TransportFactory)TRANSPORT_FACTORY_FINDER.newInstance(scheme);
TRANSPORT_FACTORYS.put(scheme, tf);
} catch (Throwable e) {
throw IOExceptionSupport.create("Transport scheme NOT recognized: [" + scheme + "]", e);
}
}
return tf;
}
//这里面的实例全部保存在META-INF目录下,通过服务发现的方式在初始化阶段加载
private static final ConcurrentMap<String, TransportFactory> TRANSPORT_FACTORYS = new ConcurrentHashMap<String, TransportFactory>();
public Transport doConnect(URI location) throws Exception {
try {
//这里面有一个options,是从uri中解析得来。
Map<String, String> options = new HashMap<String, String>(URISupport.parseParameters(location));
if( !options.containsKey("wireFormat.host") ) {
options.put("wireFormat.host", location.getHost());
}
WireFormat wf = createWireFormat(options);
Transport transport = createTransport(location, wf);
Transport rc = configure(transport, wf, options);
//remove auto
IntrospectionSupport.extractProperties(options, "auto.");
if (!options.isEmpty()) {
throw new IllegalArgumentException("Invalid connect parameters: " + options);
}
return rc;
} catch (URISyntaxException e) {
throw IOExceptionSupport.create(e);
}
}
SslTransportFactory
既然我们使用的是SSL,那么很明显就是用TransportFactory的具体实现SslTransportFactory进行Transport的创建了,进入SslTransportFactory的源码进一步追踪
@Override
protected Transport createTransport(URI location, WireFormat wf) throws UnknownHostException, IOException {
URI localLocation = null;
String path = location.getPath();
// see if the path is a local URI location
if (path != null && path.length() > 0) {
int localPortIndex = path.indexOf(':');
try {
Integer.parseInt(path.substring(localPortIndex + 1, path.length()));
String localString = location.getScheme() + ":/" + path;
localLocation = new URI(localString);
} catch (Exception e) {
LOG.warn("path isn't a valid local location for SslTransport to use", e);
}
}
SocketFactory socketFactory = createSocketFactory();
return new SslTransport(wf, (SSLSocketFactory)socketFactory, location, localLocation, false);
}
@Override
public SslTransport createTransport(WireFormat wireFormat, Socket socket, InitBuffer initBuffer)
throws IOException {
return new SslTransport(wireFormat, (SSLSocket)socket, initBuffer);
}
SslTransport
进一步追踪到SslTransport代码中,发现有对hostname进行校验的代码逻辑,是根据uri中的参数来判断
@Override
protected void initialiseSocket(Socket sock) throws SocketException, IllegalArgumentException {
/**
* This needs to default to null because this transport class is used for both a server transport
* and a client connection and we have different defaults for both.
* If we default it to a value it might override the transport server setting
* that was configured inside TcpTransportServer (which sets a default to false for server side)
*
* The idea here is that if this is a server transport then verifyHostName will be set by the setter
* and not be null as TcpTransportServer will set a default value of false (or a user will set it
* using transport.verifyHostName) but if this is a client connection the value will be null by default
* and will stay null if the user uses socket.verifyHostName to set the value or doesn't use the setter
* If it is null then we can check socketOptions for the value and if not set there then we can
* just set a default of true as this will be a client
*
* Unfortunately we have to do this to stay consistent because every other SSL option on the client
* side can be configured using socket. but this particular option isn't actually part of the socket
* so it makes it tricky from a user standpoint. For consistency sake I think it makes sense to allow
* using the socket. prefix that has been established so users do not get confused (as well as
* allow using no prefix which just calls the setter directly)
*
* Because of this there are actually two ways a client can configure this value, the client can either use
* socket.verifyHostName=<value> as mentioned or just simply use verifyHostName=<value> without using the socket.
* prefix and that will also work as the value will be set using the setter on the transport
*
* example server transport config:
* ssl://localhost:61616?transport.verifyHostName=true
*
* example from client:
* ssl://localhost:61616?verifyHostName=true
* OR
* ssl://localhost:61616?socket.verifyHostName=true
*
*/
if (verifyHostName == null) {
//Check to see if the user included the value as part of socket options and if so then use that value
if (socketOptions != null && socketOptions.containsKey("verifyHostName")) {
verifyHostName = Boolean.parseBoolean(socketOptions.get("verifyHostName").toString());
socketOptions.remove("verifyHostName");
} else {
//If null and not set then this is a client so default to true
verifyHostName = true;
}
}
if (verifyHostName) {
SSLParameters sslParams = new SSLParameters();
sslParams.setEndpointIdentificationAlgorithm("HTTPS");
((SSLSocket)this.socket).setSSLParameters(sslParams);
}
super.initialiseSocket(sock);
}
最后发现只需要在uri中增加参数即可,比如:
ssl://192.168.88.3:61617?verifyHostName=false,可是很奇怪该参数的配置并未出现在AMQ的官方文档中:https://activemq.apache.org/connection-configuration-uri
本文档详细介绍了在使用ActiveMQ时遇到的`java.security.cert.CertificateException: No name matching localhost found`问题的解决过程。内容包括SSL证书的制作、Broker SSL Connector配置、客户端代码关键部分及出现SSL问题时的排查思路,特别是如何通过修改URI参数来避免主机名匹配检查。
4988

被折叠的 条评论
为什么被折叠?



