2020年4月1日 更新:
解决在OpenJDK11下Spring Boot FatJar抛出
ClassNotFoundException的问题。详见Spring Boot Fat Jar 运行异常
问题复现
环境
AD由测试部署在Windows Server 2008上面,服务端证书也是Windows签发的
客户端:OpenJDK11(此问题在OpenJDK8+都会出现)
可以直接跳到后面的解决方案一节查看处理
通过SSL连接LDAP时,会抛出如下异常(精简后)
1 | javax.net.ssl|DEBUG|01|main|2020-01-05 13:14:47.338 CST|SSLCipher.java:437|jdk.tls.keyLimits: entry = AES/GCM/NoPadding KeyUpdate 2^37. AES/GCM/NOPADDING:KEYUPDATE = 137438953472 |
主要就是因为检查服务端证书的特定扩展失败,证书中没有对应的扩展。LDAPS对应的SSL证书需要验证IP或者DNS扩展才可以
再看下SSL流的追踪

最后显示未知证书,同时也印证了上面的异常堆栈信息。从而我们知道是证书出的问题
问题分析
定位
证书检查异常,回过头去翻一下LDAPS的RFC文档,在RFC4519第3.1.3.服务端身份认证一节,存在三种认证方式:
- 比较DNS
- 比较IP
- 比较其他SN类型
都是提取Extensions里面的subjectAlternativeName(oid: 2.5.29.17),主要涉及GeneralName的DNSName和iPAddress两种类型。当证书中不存在相应扩展,或者对应扩展的类型有误,都会校验失败
解决方案
重新实现一个SSLSocketFactory,不验证证书等信息即可:
LdapsNoVerifySSLSocketFactory.java
1 | import java.io.IOException; |
JNDI连接LDAP示例代码:
1 | import javax.naming.Context; |
附:JNDI LDAP连接流程
先创建一个配置的Map,这里用的是HashTable,因为上下文初始化的方法签名是hashtable
1 | // 初始化的方法签名 |
若选择LDAPS,最少需要如下参数
1 | # 初始化上下文工厂类, javax内部类 |
其他参数都是可以不传,因为内部有相应的判断,可以省去部分的配置,如
1 | // LdapCtx.java L2723-2742 |
在创建SSLSocket时,是在Connection.java中,方法签名如下:
1 | com.sun.jndi.ldap.Connection#createSocket(String host, int port, String socketFactory, |
不能通过OOP的方式建立SSLSocket,因为会通过反射的方式创建SSL
1 | // Connection.java L273-L278 |
SSLSocketFactory内会创建默认的SSLSocket,除非我们指定SSLSocketFactory
1 | // SSLSocketFactory L95 |
所以我们在LdapsNoVerifySSLSocketFactory里面通过静态代码块初始化配置了需要加载的类
另一种实现
所以这里引出另一种实现方式,可以减少代码量。但是耦合度较高,那就是在JNDI初始化前,初始化SSLContext,并设置为默认
注:还是需要NoVerificationTrustManager.class(定义在了LdapsNoVerifySSLSocketFactory内部)
1 | import java.security.SecureRandom; |
两种方式都可,选择适合自己的就可以啦!🔚
[Bug Fix] Spring Boot Fat Jar 运行异常
抛出问题如下:
1 | javax.naming.CommunicationException: 10.20.70.72:636 [Root exception is java.net.SocketException: java.lang.ClassNotFoundException: io.gsealy.LdapsNoVerifySSLSocketFactory] |
首先见到ClassNotFoundException就在想是不是因为类没有打进去,排查后,这种情况不存在。又试了直接打包,运行正常。本以为是Spring Boot在打包Fat Jar时候的锅,因为其特殊的打包方式,改变了正常包位置,比如说我们这里面的io.gsealy.LdapsNoVerifySSLSocketFactory类,其实是放在BOOT-INF/classes/目录下,包名也就改成了BOOT-INF.classes.io.gsealy.LdapsNoVerifySSLSocketFactory,此时我就认为是Spring Boot的锅了。
上面是完整的异常堆栈信息,具体关注这个地方:
1 | at java.base/javax.net.ssl.DefaultSSLSocketFactory.createSocket(SSLSocketFactory.java:277) |
因为ClassLoader的不同,JNDI在反射创建SSLSocketFactory时,因为安全检查的问题,无法通过反射调用方法。
1 | // Connection.java L273-L278 |
在上面的代码中。会调用getDefault()方法。因为getDefault()是一个静态方法,方法签名如下:
1 | public static SocketFactory getDefault() {} |
不是重载方法,所以最开始继承SSLSocketFactory的时候,没有修改这个方法实现,他还是会去调用SSLSocketFactory的getDefault(),也就是默认实现。默认实现是不能略过客户端证书验证的。所以会报错。
重新添加getDefault()方法即可,就可以删除静态代码块中的参数绑定了,原来的连接代码也要恢复为正常的,不需要使用另一种实现中说的实现。
修改好的文件地址:Gist Link
