LdapTemplate忽略ssl证书
一、背景
最近做JAVA的LDAP操作,使用的是Spring的LdapTemplate,基本上一个bean注入就完成了LdapTemplate的初始化,正常连接389端口,现在要要试一下HTTPS的连接方式
spring.ldap:
urls: ldap://ip:389
base: dc=xxx,dc=com
username: xxx
password: xxx
@Bean
public LdapTemplate firstLdapTemplate() {
LdapContextSource contextSource = new LdapContextSource();
contextSource.setUrl(url);
contextSource.setBase(base);
contextSource.setUserDn(username);
contextSource.setPassword(password);
contextSource.setPooled(false);
contextSource.afterPropertiesSet(); // important
LdapTemplate template = new LdapTemplate(contextSource);
return template;
}
二、采坑
把urls改成了:ldaps://xxx:636,启动报错,收到了如下错误:
Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1884)
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:276)
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:270)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1341)
at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:153)
at sun.security.ssl.Handshaker.processLoop(Handshaker.java:868)
at sun.security.ssl.Handshaker.process_record(Handshaker.java:804)
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1016)
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312)
at sun.security.ssl.SSLSocketImpl.readDataRecord(SSLSocketImpl.java:882)
at sun.security.ssl.AppInputStream.read(AppInputStream.java:102)
at java.io.BufferedInputStream.fill(BufferedInputStream.java:235)
at java.io.BufferedInputStream.read1(BufferedInputStream.java:275)
at java.io.BufferedInputStream.read(BufferedInputStream.java:334)
at com.sun.jndi.ldap.Connection.run(Connection.java:853)
at java.lang.Thread.run(Thread.java:744)
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:385)
at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:292)
at sun.security.validator.Validator.validate(Validator.java:260)
at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:326)
at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:231)
at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:126)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1323)
... 12 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:196)
at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:268)
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:380)
... 18 more
说的是校验证书错误,运维搭建的ldap服务器用的是自签的证书,也没有给我证书,所以会失败,那我就忽略证书验证吧,这样总可以吧...,google了一下,有答案,心里很开心:
LdapContextSource lcs = new LdapContextSource();
lcs.setBase("[base]");
lcs.setUserDn("[userDn]");
lcs.setPassword("[password]");
lcs.setPooled(false);
lcs.setUrl("ldaps://[server-address]:636");
DefaultTlsDirContextAuthenticationStrategy strategy = new DefaultTlsDirContextAuthenticationStrategy();
strategy.setShutdownTlsGracefully(true);
strategy.setSslSocketFactory(new CustomSSLSocketFactory()); // <-- not considered at all
strategy.setHostnameVerifier(new HostnameVerifier(){
@Override
public boolean verify(String hostname, SSLSession session){
return true;
}
});
lcs.setAuthenticationStrategy(strategy);
lcs.afterPropertiesSet();
CustomSSLSocketFactory是自定义的SSL工厂里面加载自己实现X509TrustManager,信任自签证书,然后运行,还是报错,瞬间怀疑啊,又是google一顿猛如虎的操作,有人遇到了跟我一样的问题(https://stackoverflow.com/questions/30546193/spring-ldapcontextsource-ignores-sslsocketfactory/30573130), 还好有人给出解决方法:
To fix the error use SimpleDirContextAuthenticationStrategy instead of DefaultTlsDirContextAuthenticationStrategy
但是没给出怎么用SimpleDirContextAuthenticationStrategy解决上面的问题,好吧,继续搜,找了一些答案,但是还是不行,还是继续报上面的错误,泪崩了,一直怀疑是不是自定义的CustomSSLSocketFactory与X509TrustManager有问题,而且网上的基本已经被我搜的差不多了,期间看到一个解决方案(http://java2db.com/jndi-ldap-programming/solution-to-sslhandshakeexception),我一直没试,因为不是LdapContextSource与ldaptemplate,用的是ldapContext,因为我无法通过ldapContext来构建ldaptemplate,所有就一直没试
由于怀疑CustomSSLSocketFactory与X509TrustManager有问题,然后网上又说这个方案是可以的,那就索性用来验证下我的CustomSSLSocketFactory与X509TrustManager到底有没有问题,写了个main函数,组织如下代码:
String url = "ldaps://ip:636";
String conntype = "simple";
String AdminDn = "xxx";
String password = "xxx";
Hashtable<String, String> environment = new Hashtable<String, String>();
environment.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
environment.put(Context.PROVIDER_URL,url);
environment.put("java.naming.ldap.factory.socket", CustomSSLSocketFactory.class.getName());
environment.put(Context.SECURITY_AUTHENTICATION,conntype);
environment.put(Context.SECURITY_PRINCIPAL,AdminDn);
environment.put(Context.SECURITY_CREDENTIALS, password);
ldapContext = new InitialDirContext(environment);
System.out.println("Bind successful");
果然可以!!!!
既然这个可以,那么LdapContextSource应该也可以啊,只要按照上面,把socket注入到上下文中啊,可是LdapContextSource它压根就没给我设定的接口,没办法了,只能看源码了,看看LdapContextSource的url,name,password是怎么初始化进去的
AbstractContextSource类的getAuthenticatedEnv:
protected Hashtable<String, Object> getAuthenticatedEnv(String principal, String credentials) {
// The authenticated environment should always be rebuilt.
Hashtable<String, Object> env = new Hashtable<String, Object>(getAnonymousEnv());
setupAuthenticatedEnvironment(env, principal, credentials);
return env;
}
然后看了下getAnonymousEnv是protected,完美,是不是可以继承LdapContextSource,然后重写getAnonymousEnv方法,立马测试下:
public class SSLLdapContextSource extends LdapContextSource {
public Hashtable<String, Object> getAnonymousEnv(){
Hashtable<String, Object> anonymousEnv = super.getAnonymousEnv();
anonymousEnv.put("java.naming.security.protocol", "ssl");
anonymousEnv.put("java.naming.ldap.factory.socket", CustomSSLSocketFactory.class.getName());
anonymousEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
return anonymousEnv;
}
}
@Bean
public LdapTemplate secondLdapTemplate() {
//此处用 SSLLdapContextSource
SSLLdapContextSource contextSource = new SSLLdapContextSource();
contextSource.setUrl(url);
contextSource.setBase(base);
contextSource.setUserDn(username);
contextSource.setPassword(password);
contextSource.setPooled(false);
contextSource.afterPropertiesSet(); // important
LdapTemplate template = new LdapTemplate(contextSource);
return template;
}
欧了,,只需简单的继承下LdapContextSource,前后加起来折腾了一天时间,o(╥﹏╥)o
附上CustomSSLSocketFactory:
public class CustomSSLSocketFactory extends SSLSocketFactory
{
private SSLSocketFactory socketFactory;
public CustomSSLSocketFactory()
{
try {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[]{ new DummyTrustmanager()}, new SecureRandom());
socketFactory = ctx.getSocketFactory();
} catch ( Exception ex ){ ex.printStackTrace(System.err); }
}
public static SocketFactory getDefault(){
return new CustomSSLSocketFactory();
}
@Override
public String[] getDefaultCipherSuites()
{
return socketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites()
{
return socketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket socket, String string, int num, boolean bool) throws IOException
{
return socketFactory.createSocket(socket, string, num, bool);
}
@Override
public Socket createSocket(String string, int num) throws IOException, UnknownHostException
{
return socketFactory.createSocket(string, num);
}
@Override
public Socket createSocket(String string, int num, InetAddress netAdd, int i) throws IOException, UnknownHostException
{
return socketFactory.createSocket(string, num, netAdd, i);
}
@Override
public Socket createSocket(InetAddress netAdd, int num) throws IOException
{
return socketFactory.createSocket(netAdd, num);
}
@Override
public Socket createSocket(InetAddress netAdd1, int num, InetAddress netAdd2, int i) throws IOException
{
return socketFactory.createSocket(netAdd1, num, netAdd2, i);
}
/**
* 证书
*/
public static class DummyTrustmanager implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] cert, String string) throws CertificateException
{
}
public void checkServerTrusted(X509Certificate[] cert, String string) throws CertificateException
{
}
public X509Certificate[] getAcceptedIssuers()
{
return new java.security.cert.X509Certificate[0];
}
}