【问题标题】:jndi LDAPS custom HostnameVerifier and TrustManagerjndi LDAPS 自定义 HostnameVerifier 和 TrustManager
【发布时间】:2012-03-12 18:50:14
【问题描述】:

我们正在编写一个连接到不同 LDAP 服务器的应用程序。对于每台服务器,我们可能只接受某个证书。该证书中的主机名无关紧要。当我们使用 LDAP 和 STARTTLS 时,这很容易,因为我们可以使用 StartTlsResponse.setHostnameVerifier(..-) 并使用 StartTlsResponse.negotiate(...) 和匹配的 SSLSocketFactory。但是,我们还需要支持 LDAPS 连接。 Java 本机支持这一点,但前提是服务器证书受默认 ​​java 密钥库信任。虽然我们可以替换它,但我们仍然不能为不同的服务器使用不同的密钥库。

现有的连接代码如下:

Hashtable<String,String> env = new Hashtable<String,String>();
env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
env.put( Context.PROVIDER_URL, ( encryption == SSL ? "ldaps://" : "ldap://" ) + host + ":" + port );
if ( encryption == SSL ) {
    // env.put( "java.naming.ldap.factory.socket", "CustomSocketFactory" );
}
ctx = new InitialLdapContext( env, null );
if ( encryption != START_TLS )
    tls = null;
else {
    tls = (StartTlsResponse) ctx.extendedOperation( new StartTlsRequest() );
    tls.setHostnameVerifier( hostnameVerifier );
    tls.negotiate( sslContext.getSocketFactory() );
}

我们可以添加自己的CustomSocketFactory,但是如何传递信息呢?

【问题讨论】:

    标签: java ssl ldap jndi jsse


    【解决方案1】:

    我已经构建了适合我的自己的解决方案,但它远非完美。由于javax.naming中的不幸实现,我实际上担心没有完美的解决方案。

    我的SelectiveLdapSslSocketFactory 包含一个将主机映射到不同 SSLSocketFactories 的静态 Map。当调用任何createSocket(...) 方法时,调用将委托给相应的SSLSocketFactory。它还包含一个 defaultSslSocketFactory 用于没有任何映射的主机以及 getDefaultCipherSuites()getSupportedCipherSuites() 方法。我不确定它是否正确,但在我的情况下它工作正常,所以如果你愿意,可以测试一下:

    public class SelectiveLdapSslSocketFactory extends SSLSocketFactory {
    
        private static SSLSocketFactory defaultSslSocketFactory;
        private static final Map<String, SSLSocketFactory> hostToSslSocketFactoryMap = new HashMap<>();
        
        {
            try {
                defaultSslSocketFactory = <yourOwnDefaultSslSocketFactory>;
            } catch (Exception ex) {
                Logger.warn(ex, "Couldn't initialize a defaultSslSocketFactory for LDAP connections!");
            }
        }
    
        public static SSLSocketFactory getRegisteredSslSocketFactory(String host) {
            return hostToSslSocketFactoryMap.get(host);
        }
    
        public static void registerSslSocketFactory(String host, SSLSocketFactory sslSocketFactory) {
            hostToSslSocketFactoryMap.put(host, sslSocketFactory);
        }
    
        public static void deregisterSslSocketFactory(String host) {
            hostToSslSocketFactoryMap.remove(host);
        }
    
        public SelectiveLdapSslSocketFactory() {
        }
    
        public static SocketFactory getDefault() {
            return new SelectiveLdapSslSocketFactory();
        }
    
        @Override
        public String[] getDefaultCipherSuites() {
            return defaultSslSocketFactory.getDefaultCipherSuites();
        }
    
        @Override
        public String[] getSupportedCipherSuites() {
            return defaultSslSocketFactory.getSupportedCipherSuites();
        }
    
        @Override
        public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
            SSLSocketFactory sslSocketFactory = Objects.requireNonNullElse(hostToSslSocketFactoryMap.get(host), defaultSslSocketFactory);
            return sslSocketFactory.createSocket(s, host, port, autoClose);
        }
    
        @Override
        public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
            SSLSocketFactory sslSocketFactory = Objects.requireNonNullElse(hostToSslSocketFactoryMap.get(host), defaultSslSocketFactory);
            return sslSocketFactory.createSocket(host, port);
        }
    
        @Override
        public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
            SSLSocketFactory sslSocketFactory = Objects.requireNonNullElse(hostToSslSocketFactoryMap.get(host), defaultSslSocketFactory);
            return sslSocketFactory.createSocket(host, port, localHost, localPort);
        }
    
        @Override
        public Socket createSocket(InetAddress host, int port) throws IOException {
            SSLSocketFactory sslSocketFactory = getSslSocketFactory(host);
            return sslSocketFactory.createSocket(host, port);
        }
    
        @Override
        public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
            SSLSocketFactory sslSocketFactory = getSslSocketFactory(address);
            return sslSocketFactory.createSocket(address, port, localAddress, localPort);
        }
    
        private SSLSocketFactory getSslSocketFactory(InetAddress inetAddress) {
            SSLSocketFactory sslSocketFactory = hostToSslSocketFactoryMap.get(Objects.requireNonNullElse(inetAddress.getCanonicalHostName(), ""));
            if (sslSocketFactory == null) {
                sslSocketFactory = hostToSslSocketFactoryMap.get(Objects.requireNonNullElse(inetAddress.getHostName(), ""));
                if (sslSocketFactory == null) {
                    sslSocketFactory = hostToSslSocketFactoryMap.get(Objects.requireNonNullElse(inetAddress.getHostAddress(), ""));
                    if (sslSocketFactory == null) {
                        sslSocketFactory = defaultSslSocketFactory;
                    }
                }
            }
            return sslSocketFactory;
        }
    }
    

    然后你可以像这样使用它:

    ...
    SelectiveLdapSslSocketFactory.registerSslSocketFactory(host01, sslSocketFactory01);
    SelectiveLdapSslSocketFactory.registerSslSocketFactory(host02, sslSocketFactory02);
    SelectiveLdapSslSocketFactory.registerSslSocketFactory(host03, sslSocketFactory03);
    
    props.put("java.naming.ldap.factory.socket", SelectiveLdapSslSocketFactory.class.getName());
    

    【讨论】:

      【解决方案2】:

      对于其他人有同样的问题:我为我的案例找到了一个非常丑陋的解决方案:

      import javax.net.SocketFactory;
      
      public abstract class ThreadLocalSocketFactory
        extends SocketFactory
      {
      
        static ThreadLocal<SocketFactory> local = new ThreadLocal<SocketFactory>();
      
        public static SocketFactory getDefault()
        {
          SocketFactory result = local.get();
          if ( result == null )
            throw new IllegalStateException();
          return result;
        }
      
        public static void set( SocketFactory factory )
        {
          local.set( factory );
        }
      
        public static void remove()
        {
          local.remove();
        }
      
      }
      

      像这样使用它:

      env.put( "java.naming.ldap.factory.socket", ThreadLocalSocketFactory.class.getName() );
      ThreadLocalSocketFactory.set( sslContext.getSocketFactory() );
      try {
        ctx = new InitialLdapContext( env, null );
      } finally {
        ThreadLocalSocketFactory.remove();
      }
      

      不太好,但它有效。 JNDI 在这里应该更灵活...

      【讨论】:

      • 所以基本上,你已经产生了你自己的答案,这在我的答案的延续中是正确的,你说你已经否决了......
      • 我从来没有说过你写的东西是错的,只是我的问题中已经提到了这些信息,并没有以任何方式帮助解决问题。这就是我投反对票的原因。如果这不是反对投票的有效理由,我会删除该反对票,但是 EJP 赞成票(这只是为了清除我的反对票)将保留,投票赞成一个无用的答案,所以我不会。
      • 正如我所说,在编辑之前足够公平,但是一旦你得到一个答案,它会在 OpenJDK 代码中向你展示为什么你不能做你想做的事情,除非你使用静态成员(你有完成)或类似 JDNI 之类的东西,我认为这是对您问题的回答。然后,您已经接受了自己的答案,这很明显符合我的建议。我不太关心否决票,我不介意您接受了自己的答案这一事实(因为您对ThreadLocal 更具体)。我只是觉得投反对票+接受自己的答案来自投反对票的答案很奇怪。
      【解决方案3】:

      您应该传递自己的 SSLSocketFactory 子类的名称并将其完全限定的名称传递给 "java.naming.ldap.factory.socket" env 属性,如 "Using Custom Sockets" section of the Java LDAP/SSL guide 中所述:

      env.put("java.naming.ldap.factory.socket", "example.CustomSocketFactory");
      

      您不能将任何特定参数传递给此类,请参阅com.sun.jndi.ldap.Connection.createSocket(...) 中的实例化:

      Class socketFactoryClass = Obj.helper.loadClass(socketFactory);
      Method getDefault =
          socketFactoryClass.getMethod("getDefault", new Class[]{});
      Object factory = getDefault.invoke(null, new Object[]{});
      

      如果您需要额外的参数,您可能不得不使用静态成员或 JNDI(通常不理想)。

      据我所知,不幸的是,在此实现中使用ldaps:// 时似乎没有任何主机名验证。如果您只信任信任管理器中的一个显式证书,这应该可以弥补主机名验证的不足。

      【讨论】:

      • 我在上面的清单中有这行代码。但是,由于我只能指定一个类名而不是某个实例,因此我无法将参数传递给该类——这正是我所需要的。你读过这个问题吗?如果是这样,请告诉我应该在哪里更详细。
      • @SteffenHeil,对不起,我回答得太快了,我已经添加了更多细节。
      • 我必须连接到服务器 S1 并且只接受证书 C1 并连接到服务器 S2 并且只接受证书 C2。使用 CustomSocketFactory 应该如何工作?
      • 这个答案中提到的每一种可能性都在我的问题中。所以我对这个答案投了反对票……
      • @SteffenHeil,在编辑之前足够公平,但是当答案告诉你在引用实现你所追求的代码后你不能做某事时(并且没有按照你想要的方式编码),它不值得被否决(更多的是赞成)。无论如何,我不会担心一票...
      猜你喜欢
      • 2014-06-28
      • 1970-01-01
      • 1970-01-01
      • 2017-07-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多