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

Posted

技术标签:

【中文标题】如何通过 LDAP over TLS 对 Active Directory 进行身份验证?【英文标题】:How to authenticate against Active Directory via LDAP over TLS? 【发布时间】:2013-05-10 07:55:40 【问题描述】:

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

我已经在 Eclipse 中安装了 LDAP 浏览器,并且我确实可以在 it 中使用 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)?

感谢任何帮助。

【问题讨论】:

【参考方案1】:

好的,经过大约一天半的工作,我想通了。

我原来的做法是扩展 Spring 的 ActiveDirectoryLdapAuthenticationProvider 类,并覆盖它的 loadUserAuthorities() 方法,从而自定义认证用户权限的构建方式。由于不明显的原因,ActiveDirectoryLdapAuthenticationProvider 类被指定为final,所以我当然不能扩展它。

谢天谢地,开源提供了黑客攻击(并且该类的超类不是final),所以我只是复制了它的全部内容,删除了final 名称,并调整了相应的包和类引用。我没有在这个类中编辑任何代码,除了添加一个高度可见的注释,说不要编辑它。然后我在OverrideActiveDirectoryLdapAuthenticationProvider 中扩展了这个类,我也在我的ldap.xml 文件中引用了它,并在其中为loadUserAuthorities 添加了一个覆盖方法。通过未加密会话(在隔离的虚拟服务器上)上的简单 LDAP 绑定,所有这些都非常有效。

真实的网络环境要求所有的 LDAP 查询都以 TLS 握手开始,但是,被查询的服务器不是 PDC——它的名称是 'sub.domain.tld',但用户通过了正确的身份验证域.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 传递的;我只是将参数 there 更改为 &lt;constructor-arg value="domain.tld"/&gt;

然后我改了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;

通过上述更改——仍然需要从我的所有测试和 headdesking 中清理——我能够在网络上对 Active Directory 进行绑定和身份验证,捕获我希望的任何用户对象字段,识别组成员身份等。

哦,显然 TLS 不需要 'ldaps://',所以我的 ldap.xml 只需 ldap://192.168.0.3:389


tl;dr

要启用 TLS,请复制 Spring 的 ActiveDirectoryLdapAuthenticationProvider 类,删除 final 名称,在自定义类中扩展它,并通过将 env.put(Context.SECURITY_PROTOCOL, "tls"); 添加到环境哈希表来覆盖 bindAsUser()。就是这样。

要更紧密地控制绑定用户名、域和 LDAP 查询字符串,请酌情覆盖适用的方法。就我而言,我无法确定 0 的值是什么,所以我将其完全删除并插入了传递的 username 字符串。

希望有人会觉得这很有帮助。

【讨论】:

【参考方案2】:

或者,如果您不介意使用 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。自然它是可选的,可以很容易地删除。

【讨论】:

以上是关于如何通过 LDAP over TLS 对 Active Directory 进行身份验证?的主要内容,如果未能解决你的问题,请参考以下文章

如何解决 PHP 中的 ldap_start_tls()“无法启动 TLS:连接错误”?

Spring Security with LDAP over SSL:需要更多细节

mqtt使用WebSocket over TLS(wss)握手失败

19LDAP TLS配置

centos7.2 LDAP(TLS)+autofs+ssh

如何将 ActiveDirectory 与 Spring-Security LDAP 一起使用