一、背景
最近做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];
}
}