如何通过LDAP over TLS对Active Directory进行身份验证?

4

我有一个工作中的概念验证应用程序,可以在测试服务器上通过LDAP成功地对Active Directory进行身份验证,但生产应用程序必须通过TLS进行身份验证 - 域控制器会关闭任何未通过TLS发起的连接。

我已经在Eclipse中安装了LDAP浏览器,并且我确实可以使用TLS绑定自己,但是我无法想出如何让我的应用程序使用TLS。

ldap.xml:

<bean id="ldapAuthenticationProvider"
        class="my.project.package.OverrideActiveDirectoryLdapAuthenticationProvider">

    <!-- this works to authenticate by binding as the user in question -->
    <constructor-arg value="test.server"/>
    <constructor-arg value="ldap://192.168.0.2:389"/>

    <!-- this doesn't work, because the server requires a TLS connection -->
    <!-- <constructor-arg value="production.server"/> -->
    <!-- <constructor-arg value="ldaps://192.168.0.3:389"/> -->

    <property name="convertSubErrorCodesToExceptions" value="true"/>
</bean>
OverrideActiveDirectoryLdapAuthenticationProvider是一个覆盖类,它扩展了Spring的ActiveDirectoryLdapAuthenticationProvider类的副本,这个类由于某种原因被标记为final。我之所以要进行覆盖,是为了定制权限/权限如何在用户对象上填充(我们将使用相关组的组成员身份来构建用户的权限,或者我们将从AD用户对象的字段中读取)。在其中,我只重载了loadUserAuthorities()方法,但我怀疑我可能还需要重载bindAsUser()方法或者可能是doAuthentication()方法。

XML和一个覆盖类是我的应用程序管理身份验证的唯一两个地方,而不是让Spring来处理。我已经在几个地方阅读到,要启用TLS,我需要扩展DefaultTlsDirContextAuthenticationStrategy类,但我应该把它连到哪里?有一个命名空间解决方案吗?或者我需要完全做其他事情(例如,放弃使用Spring的ActiveDirectoryLdapAuthenticationProvider,而改用LdapAuthenticationProvider

非常感谢您的帮助。

2个回答

9
好的,经过大约一天半的工作,我终于解决了问题。
我的原始方法是扩展Spring的ActiveDirectoryLdapAuthenticationProvider类,并覆盖其loadUserAuthorities()方法,以便自定义构建认证用户权限的方式。由于一些不明显的原因,ActiveDirectoryLdapAuthenticationProvider类被指定为final,所以我无法对其进行扩展。
值得庆幸的是,开源提供了黑客的方法(该类的超类并非final),所以我只需复制整个内容,删除final指示,并相应调整包和类引用。我没有编辑此类中的任何代码,除了添加一个高度可见的注释,其中写着不要编辑它。然后,在OverrideActiveDirectoryLdapAuthenticationProvider中扩展了这个类,我还在其中添加了一个loadUserAuthorities的重载方法,并在我的ldap.xml文件中引用了它。所有这些都可以通过简单的LDAP绑定在未加密会话上(在隔离的虚拟服务器上)很好地工作。
真正的网络环境要求所有LDAP查询都必须从TLS握手开始,而被查询的服务器不是PDC——它的名称是'sub.domain.tld',但用户已正确验证为'domain.tld'。此外,用户名必须以'NT_DOMAIN\'开头才能进行绑定。所有这些都需要自定义工作,而不幸的是,在任何地方我都找不到太多帮助。
所以这里是可笑简单的更改,所有这些都涉及在OverrideActiveDirectoryLdapAuthenticationProvider中进一步重载:
@Override
protected DirContext bindAsUser(String username, String password) {
    final String bindUrl = url; //super reference
    Hashtable<String,String> env = new Hashtable<String,String>();
    env.put(Context.SECURITY_AUTHENTICATION, "simple");
    //String bindPrincipal = createBindPrincipal(username);
    String bindPrincipal = "NT_DOMAIN\\" + username; //the bindPrincipal() method builds the principal name incorrectly
    env.put(Context.SECURITY_PRINCIPAL, bindPrincipal);
    env.put(Context.PROVIDER_URL, bindUrl);
    env.put(Context.SECURITY_CREDENTIALS, password);
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxtFactory");
    //and finally, this simple addition
    env.put(Context.SECURITY_PROTOCOL, "tls");

    //. . . try/catch portion left alone
}

也就是说,我所做的只是更改了bindPrincipal字符串的格式,并向哈希表中添加了一个键/值对。

我不需要从传递给我的类的domain参数中删除子域名,因为这是通过ldap.xml传递的;我只是在那里将参数更改为<constructor-arg value="domain.tld"/>

然后我更改了OverrideActiveDirectoryLdapAuthenticationProvider中的searchForUser()方法:

@Override
protected DirContextOperations searchForUser(DirContext ctx, String username) throws NamingException {
    SearchControls searchCtls = new SearchControls();
    searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);

    //this doesn't work, and I'm not sure exactly what the value of the parameter {0} is
    //String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))";
    String searchFilter = "(&(objectClass=user)(userPrincipalName=" + username + "@domain.tld))";

    final String bindPrincipal = createBindPrincipal(username);
    String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal);

    return SpringSecurityLdapTemplate.searchForSingleEntryInternal(ctx, searchCtls, searchRoot, searchFilter, new Object[]{bindPrincipal});

最后一次更改是对createBindPrincipal()方法进行修改,以便正确地构建字符串(根据我的需求):
@Override
String createBindPrincipal(String username) {
    if (domain == null || username.toLowerCase().endsWith(domain)) {
        return username;
    }
    return "NT_DOMAIN\\" + username;
}

通过上述更改 -- 我仍然需要对所有测试和调试的内容进行清理 -- 我能够绑定并认证自己对网络中的Active Directory进行操作,捕获我想要的用户对象字段,识别组成员等等。

哦,还有明显TLS并不需要 'ldaps://',所以我的ldap.xml只需使用 ldap://192.168.0.3:389


tl;dr:

为启用TLS,请复制Spring的ActiveDirectoryLdapAuthenticationProvider类,删除final指示,将其扩展到自定义类中,并重写bindAsUser(), 同时将env.put(Context.SECURITY_PROTOCOL, "tls");添加到环境哈希表中即可。

如果要更精确地控制绑定用户名、域和LDAP查询字符串,请根据需要重写适当的方法。在我的情况下,我无法确定{0}的值是什么,所以我完全移除了它并插入了传递的username字符串。

希望这对某些人有所帮助。


0

或者,如果您不介意使用spring-ldap并在org.springframework.security.ldap.authentication.ad下创建一个工厂类,也可以通过覆盖contextFactory来“黑掉”ActiveDirectoryLdapAuthenticationProvider,这是允许测试目的的包保护访问,使用以下内容:

package org.springframework.security.ldap.authentication.ad;

import lombok.experimental.UtilityClass;

@UtilityClass
public class ActiveDirectoryLdapAuthenticationProviderFactory {
    private final TlsContextFactory TLS_CONTEXT_FACTORY = new TlsContextFactory();

    public ActiveDirectoryLdapAuthenticationProvider create(String domain, String url, boolean startTls) {
        final var authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(domain, url);
        if (startTls) {
            authenticationProvider.contextFactory = TLS_CONTEXT_FACTORY;
        }
        return authenticationProvider;
    }
}

package org.springframework.security.ldap.authentication.ad;

import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import java.util.Hashtable;

class TlsContextFactory extends ActiveDirectoryLdapAuthenticationProvider.ContextFactory {
    private static final DefaultTlsDirContextAuthenticationStrategy TLS_DIR_CONTEXT_AUTHENTICATION_STRATEGY = new DefaultTlsDirContextAuthenticationStrategy();

    @Override
    DirContext createContext(Hashtable<?, ?> env) throws NamingException {
        final var username = (String) env.remove(Context.SECURITY_PRINCIPAL);
        final var password = (String) env.remove(Context.SECURITY_CREDENTIALS);
        final var context = super.createContext(env);
        return TLS_DIR_CONTEXT_AUTHENTICATION_STRATEGY.processContextAfterCreation(context, username, password);
    }
}

额外内容:如果您不想处理证书/命名问题,这通常是AD的情况,您可以选择以下内容:

package org.springframework.security.ldap.authentication.ad;

import com.acme.IgnoreAllTlsDirContextAuthenticationStrategy;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import java.util.Hashtable;

class TlsContextFactory extends ActiveDirectoryLdapAuthenticationProvider.ContextFactory {
    private static final IgnoreAllTlsDirContextAuthenticationStrategy TLS_DIR_CONTEXT_AUTHENTICATION_STRATEGY = new IgnoreAllTlsDirContextAuthenticationStrategy();

    @Override
    DirContext createContext(Hashtable<?, ?> env) throws NamingException {
        final var username = (String) env.remove(Context.SECURITY_PRINCIPAL);
        final var password = (String) env.remove(Context.SECURITY_CREDENTIALS);
        final var context = super.createContext(env);
        return TLS_DIR_CONTEXT_AUTHENTICATION_STRATEGY.processContextAfterCreation(context, username, password);
    }
}

package com.acme;

import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy;

public class IgnoreAllTlsDirContextAuthenticationStrategy extends DefaultTlsDirContextAuthenticationStrategy {
    public IgnoreAllTlsDirContextAuthenticationStrategy() {
        setHostnameVerifier((hostname, session) -> true);
        setSslSocketFactory(new NonValidatingSSLSocketFactory());
    }
}

package com.acme;

import lombok.SneakyThrows;
import lombok.experimental.Delegate;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;

public class NonValidatingSSLSocketFactory extends SSLSocketFactory {
    @Delegate
    private final SSLSocketFactory delegateSocketFactory;

    @SneakyThrows
    public NonValidatingSSLSocketFactory() {
        SSLContext ctx = SSLContext.getInstance("TLS");

        ctx.init(null, new TrustManager[]{new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) {
            }

            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }
        }}, null);

        delegateSocketFactory = ctx.getSocketFactory();
    }
}

PS:为了代码可读性,使用了Lombok。当然,它是可选的,可以轻松地删除。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接