最近、ちょっとLDAP…LDAPSでの接続とかをやってみたので、メモとして。
LDAPおよびLDAPSでの接続を、Javaから簡単な例で書いてみます。
こちらを参考に。
環境
$ java -version openjdk version "1.8.0_171" OpenJDK Runtime Environment (build 1.8.0_171-8u171-b11-0ubuntu0.16.04.1-b11) OpenJDK 64-Bit Server VM (build 25.171-b11, mixed mode) $ mvn -v Apache Maven 3.5.3 (3383c37e1f9e9b3bc3df5050c29c8aff9f295297; 2018-02-25T04:49:05+09:00) Maven home: /usr/local/maven3/current Java version: 1.8.0_171, vendor: Oracle Corporation Java home: /usr/lib/jvm/java-8-openjdk-amd64/jre Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "4.4.0-104-generic", arch: "amd64", family: "unix"
LDAPサーバーについては、OpenLDAPのDockerイメージを使いました。
Dockerイメージの起動コマンドは、こちらで。
$ docker run -it --rm --name openldap --env LDAP_ADMIN_PASSWORD="admin-password" --env LDAP_DOMAIN=test.example.com --env LDAP_TLS_VERIFY_CLIENT=try osixia/openldap:1.2.0
ドメインは「test.example.com」にして、adminのパスワードは「admin-password」へ、あとTLSの設定を。
準備
Maven依存関係は、こちら。
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.2.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.10.0</version> <scope>test</scope> </dependency>
test.ldif
dn: uid=user001,dc=test,dc=example,dc=com objectClass: inetOrgPerson objectClass: posixAccount uid: user001 cn: テスト 太郎 sn: テスト uidNumber: 10001 gidNumber: 10001 homeDirectory: /home/user001 userPassword: {SSHA}K1h08ZgBJQIInrqH1eerLG/I4jO2H9fh description: My Test Account
追加。
$ ldapadd -f test.ldif -D "cn=admin,dc=test,dc=example,dc=com" -w admin-password adding new entry "uid=user001,dc=test,dc=example,dc=com"
確認(Dockerコンテナ内で実行)。
## LDAP $ ldapsearch -x -H ldap://localhost -b uid=user001,dc=test,dc=example,dc=com -D "cn=admin,dc=test,dc=example,dc=com" -w admin-password # extended LDIF # # LDAPv3 # base <uid=user001,dc=test,dc=example,dc=com> with scope subtree # filter: (objectclass=*) # requesting: ALL # # user001, test.example.com dn: uid=user001,dc=test,dc=example,dc=com objectClass: inetOrgPerson objectClass: posixAccount uid: user001 cn:: 44OG44K544OIIOWkqumDjg== sn:: 44OG44K544OI uidNumber: 10001 gidNumber: 10001 homeDirectory: /home/user001 userPassword:: e1NIQX1hUnloRlJkSXJudFNzY2d1cXF6R0Q0MUIyNEk9 description: My Test Account # search result search: 2 result: 0 Success # numResponses: 2 # numEntries: 1 ## LDAPS $ ldapsearch -x -H ldap://localhost -b uid=user001,dc=test,dc=example,dc=com -D "cn=admin,dc=test,dc=example,dc=com" -w admin-password # extended LDIF # # LDAPv3 # base <uid=user001,dc=test,dc=example,dc=com> with scope subtree # filter: (objectclass=*) # requesting: ALL # # user001, test.example.com dn: uid=user001,dc=test,dc=example,dc=com objectClass: inetOrgPerson objectClass: posixAccount uid: user001 cn:: 44OG44K544OIIOWkqumDjg== sn:: 44OG44K544OI uidNumber: 10001 gidNumber: 10001 homeDirectory: /home/user001 userPassword:: e1NIQX1hUnloRlJkSXJudFNzY2d1cXF6R0Q0MUIyNEk9 description: My Test Account # search result search: 2 result: 0 Success # numResponses: 2 # numEntries: 1
テストコードの雛形
実行は、テストコードで行っていきます。雛形は、こんな感じで。
src/test/java/org/liittlewings/ldap/example/SimpleLdapTest.java
package org.liittlewings.ldap.example; import java.util.Hashtable; import javax.naming.CommunicationException; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import javax.net.ssl.SSLHandshakeException; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; public class SimpleLdapTest { // ここに、テストを書く!! }
LDAP接続
LDAPへの接続コードは、こちらも合わせて参考に。
JavaでLDAP認証をやってみる - 眩しいサインを見ただろう
JavaでActiveDirectory検索を行う(AD認証) | 株式会社アースリンク
LDAP接続用コード | 寺田 佳央 - Yoshio Terada
adminと、追加ユーザーそれぞれで接続できれば、OKとしましょう。
で、作ったのはこんなコード。
@Test public void ldapGettingStarted() throws NamingException { String url = "ldap://172.17.0.2:389"; Hashtable<String, String> env = new Hashtable<>(); env.put(Context.PROVIDER_URL, url); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, "cn=admin,dc=test,dc=example,dc=com"); env.put(Context.SECURITY_CREDENTIALS, "admin-password"); DirContext ctx = new InitialDirContext(env); try { SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); NamingEnumeration<SearchResult> searchResult = ctx.search("dc=test,dc=example,dc=com", "uid=user001", searchControls); assertThat(searchResult.hasMoreElements()).isTrue(); assertThat(searchResult.nextElement().getAttributes().get("uid").get()).isEqualTo("user001"); Hashtable<String, String> uenv = new Hashtable<>(env); uenv.put(Context.SECURITY_PRINCIPAL, "uid=user001,dc=test,dc=example,dc=com"); uenv.put(Context.SECURITY_CREDENTIALS, "user-password"); DirContext uctx = new InitialDirContext(uenv); // ok uctx.close(); } finally { ctx.close(); } }
はい。
LDAPS(自己署名証明書)
続いて、LDAPS。ですが、証明書が正規のものではないため、SSLハンドシェイクで失敗します。
@Test public void ldapsGettingStarted() throws NamingException { String url = "ldaps://172.17.0.2:636"; Hashtable<String, String> env = new Hashtable<>(); env.put(Context.PROVIDER_URL, url); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, "cn=admin,dc=test,dc=example,dc=com"); env.put(Context.SECURITY_CREDENTIALS, "admin-password"); assertThatThrownBy(() -> new InitialDirContext(env)) .isInstanceOf(CommunicationException.class) .hasMessage("simple bind failed: 172.17.0.2:636") .hasCauseInstanceOf(SSLHandshakeException.class); // 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 }
さてどうしましょう、ということで、証明書をkeystoreに追加してもいいのですが、ここはコードで解決してみましょう。
「java.naming.ldap.factory.socket」で、SocketFactoryを指定できるようなので、カスタムなSSLSocketFactoryを作成してこちらを使用するようにしてみます。
src/main/java/org/littlewings/ldap/example/LooseSSLSocketFactory.java
package org.littlewings.ldap.example; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.SocketFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; public class LooseSSLSocketFactory extends SSLSocketFactory { SSLSocketFactory delegate; public LooseSSLSocketFactory() { try { SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(null, new X509TrustManager[]{ new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return null; } } }, new SecureRandom()); delegate = sslContext.getSocketFactory(); } catch (Exception e) { throw new RuntimeException(e); } } @Override public String[] getDefaultCipherSuites() { return delegate.getDefaultCipherSuites(); } @Override public String[] getSupportedCipherSuites() { return delegate.getSupportedCipherSuites(); } @Override public Socket createSocket(Socket socket, String s, int i, boolean b) throws IOException { return delegate.createSocket(socket, s, i, b); } @Override public Socket createSocket(String s, int i) throws IOException, UnknownHostException { return delegate.createSocket(s, i); } @Override public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) throws IOException, UnknownHostException { return delegate.createSocket(s, i, inetAddress, i1); } @Override public Socket createSocket(InetAddress inetAddress, int i) throws IOException { return delegate.createSocket(inetAddress, i); } @Override public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) throws IOException { return delegate.createSocket(inetAddress, i, inetAddress1, i1); } public static SocketFactory getDefault() { return new LooseSSLSocketFactory(); } }
このSSLSocketFactoryを使うように、修正したコード。
@Test public void ldapGettingStartedWithCustomSSLSocketFactory() throws NamingException { String url = "ldaps://172.17.0.2:636"; Hashtable<String, String> env = new Hashtable<>(); env.put(Context.PROVIDER_URL, url); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, "cn=admin,dc=test,dc=example,dc=com"); env.put(Context.SECURITY_CREDENTIALS, "admin-password"); env.put("java.naming.ldap.factory.socket", "org.littlewings.ldap.example.LooseSSLSocketFactory"); DirContext ctx = new InitialDirContext(env); try { SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); NamingEnumeration<SearchResult> searchResult = ctx.search("dc=test,dc=example,dc=com", "uid=user001", searchControls); assertThat(searchResult.hasMoreElements()).isTrue(); SearchResult searchResult1 = searchResult.nextElement(); assertThat(searchResult1.getAttributes().get("uid").get()).isEqualTo("user001"); Hashtable<String, String> uenv = new Hashtable<>(env); uenv.put(Context.SECURITY_PRINCIPAL, "uid=user001,dc=test,dc=example,dc=com"); uenv.put(Context.SECURITY_CREDENTIALS, "user-password"); DirContext uctx = new InitialDirContext(uenv); // ok uctx.close(); } finally { ctx.close(); } }
追加したのは、この部分ですね。
env.put("java.naming.ldap.factory.socket", "org.littlewings.ldap.example.LooseSSLSocketFactory");
LDAPに慣れていないのでだいぶてこずりましたが、とりあえずはこんなところで。