简体   繁体   English

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

[英]How to authenticate against Active Directory via LDAP over TLS?

I have a working proof-of-concept application which can successfully authenticate against Active Directory via LDAP on a test server, but the production application will have to do so over TLS -- the domain controller closes any connection which does not initiate via TLS.我有一个有效的概念验证应用程序,它可以在测试服务器上通过 LDAP 成功通过 Active Directory 进行身份验证,但生产应用程序必须通过 TLS 执行此操作——域控制器关闭任何不通过 TLS 启动的连接。

I have installed the LDAP browser in Eclipse, and I can indeed bind as myself using TLS in it , but I cannot for the life of me figure out how to get my application to use TLS.我已经在 Eclipse 中安装了 LDAP 浏览器,我确实可以在其中使用 TLS 进行绑定,但我终生无法弄清楚如何让我的应用程序使用 TLS。

ldap.xml : 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 is an override class which extends a copy of Spring's ActiveDirectoryLdapAuthenticationProvider class, which is for some reason designated final . OverrideActiveDirectoryLdapAuthenticationProvider是一个覆盖类,它扩展了 Spring 的ActiveDirectoryLdapAuthenticationProvider类的副本,由于某种原因,它被指定为final My reasons for overriding have to do with customizing the way permissions/authorities are populated on the user object (we will either use group membership of relevant groups to build the user's permissions, or we will read from a field on the AD user object).我覆盖的原因与自定义在用户对象上填充权限/权限的方式有关(我们将使用相关组的组成员身份来构建用户的权限,或者我们将从 AD 用户对象上的字段中读取)。 In it, I'm only overriding the loadUserAuthorities() method, but I suspect I may also need to override the bindAsUser() method or possibly the doAuthentication() method.在其中,我只覆盖了loadUserAuthorities()方法,但我怀疑我可能还需要覆盖bindAsUser()方法或可能的doAuthentication()方法。

The XML and one override class are the only two places where authentication is being managed by my application as opposed to letting Spring do the work. XML 和一个覆盖类是我的应用程序管理身份验证而不是让 Spring 完成工作的仅有的两个地方。 I've read several places that to enable TLS I need to extend the DefaultTlsDirContextAuthenticationStrategy class, but where do I wire it in?我已经阅读了几个地方,启用 TLS 我需要扩展DefaultTlsDirContextAuthenticationStrategy类,但是我在哪里连接它? Is there a namespace solution?有命名空间解决方案吗? Do I need to do something else entirely (ie abandon the use of Spring's ActiveDirectoryLdapAuthenticationProvider and instead use LdapAuthenticationProvider )?我是否需要完全做其他事情(即放弃使用 Spring 的ActiveDirectoryLdapAuthenticationProvider而是使用LdapAuthenticationProvider )?

Any help is appreciated.任何帮助表示赞赏。

Okay, so after about a day and a half of working on it, I figured it out.好的,经过大约一天半的工作,我想通了。

My original approach was to extend Spring's ActiveDirectoryLdapAuthenticationProvider class, and override its loadUserAuthorities() method, so as to customize the way the authenticated user's permissions were built.我最初的方法是扩展 Spring 的ActiveDirectoryLdapAuthenticationProvider类,并覆盖其loadUserAuthorities()方法,以便自定义构建经过身份验证的用户权限的方式。 For unobvious reasons, the ActiveDirectoryLdapAuthenticationProvider class is designated as final , so of course I cannot extend it.由于不明显的原因, ActiveDirectoryLdapAuthenticationProvider类被指定为final ,所以我当然不能扩展它。

Thankfully, open source provides for hacking (and that class' superclasses are not final ), so I simply copied the entire contents of it, removed the final designation, and adjusted the package and class references accordingly.值得庆幸的是,开源提供了黑客攻击(并且该类的超类不是final ),所以我简单地复制了它的全部内容,删除了final名称,并相应地调整了包和类引用。 I did not edit any code in this class, except to add a highly visible comment which says not to edit it.我没有编辑这个类中的任何代码,只是添加了一个高度可见的注释,说不要编辑它。 I then extended this class in OverrideActiveDirectoryLdapAuthenticationProvider , which I also referenced in my ldap.xml file, and in it added an override method for loadUserAuthorities .然后我在OverrideActiveDirectoryLdapAuthenticationProvider扩展了这个类,我也在我的ldap.xml文件中引用了它,并在其中添加了一个用于loadUserAuthorities的覆盖方法。 All of that worked great with a simple LDAP bind over an unencrypted session (on an isolated virtual server).在未加密的会话(在隔离的虚拟服务器上)上进行简单的 LDAP 绑定时,所有这些都非常有效。

The real network environment requires that all LDAP queries begin with a TLS handshake, however, and the server being queried is not the PDC -- its name is 'sub.domain.tld`, but the user is properly authenticated against 'domain.tld.'然而,真实的网络环境要求所有 LDAP 查询都以 TLS 握手开始,并且被查询的服务器不是 PDC——它的名称是“sub.domain.tld”,但用户已针对“domain.tld”进行了正确的身份验证.' Also, the username must be prepended with 'NT_DOMAIN\\' in order to bind.此外,用户名前必须加上“NT_DOMAIN\\”才能绑定。 All of this required customization work, and unfortunately, I found little or no help anywhere.所有这些都需要定制工作,不幸的是,我在任何地方都几乎没有找到任何帮助。

So here are the preposterously simple changes, all of which involve further overrides in OverrideActiveDirectoryLdapAuthenticationProvider :所以这里是荒谬的简单更改,所有这些更改都涉及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
}

That is, all I did to this method was change the way the bindPrincipal string was formatted, and I added a key/value to the hashtable.也就是说,我对这个方法所做的只是改变了bindPrincipal字符串的格式,我向哈希表添加了一个键/值。

I did not have to remove the subdomain from the domain parameter passed to my class, because that was being passed by ldap.xml ;我不必从传递给我的类的domain参数中删除子domain ,因为它是由ldap.xml传递的; I simply changed the parameter there to <constructor-arg value="domain.tld"/>我只是将那里的参数更改为<constructor-arg value="domain.tld"/>

Then I changed the searchForUser() method in OverrideActiveDirectoryLdapAuthenticationProvider :然后我更改了OverrideActiveDirectoryLdapAuthenticationProvidersearchForUser()方法:

@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});

The last change was to the createBindPrincipal() method, to build the String properly (for my purposes):最后一个更改是createBindPrincipal()方法,以正确构建字符串(出于我的目的):

@Override
String createBindPrincipal(String username) {
    if (domain == null || username.toLowerCase().endsWith(domain)) {
        return username;
    }
    return "NT_DOMAIN\\" + username;
}

And with the above changes -- which still need cleaned up from all of my testing and headdesking -- I was able to bind and authenticate as myself against Active Directory on the network-proper, capture whatever user object fields I wished, identify group membership, etc.通过上述更改——仍然需要从我的所有测试和前台清理中清除——我能够在网络上针对 Active Directory 绑定和验证我自己,捕获我希望的任何用户对象字段,识别组成员身份, 等等。

Oh, and apparently TLS does not require 'ldaps://', so my ldap.xml simply has ldap://192.168.0.3:389 .哦,显然 TLS 不需要 'ldaps://',所以我的ldap.xml只是ldap://192.168.0.3:389


tl;dr : tl;博士

To enable TLS, copy Spring's ActiveDirectoryLdapAuthenticationProvider class, remove the final designation, extend it in a custom class, and override bindAsUser() by adding env.put(Context.SECURITY_PROTOCOL, "tls");要启用 TLS,请复制 Spring 的ActiveDirectoryLdapAuthenticationProvider类,删除final名称,在自定义类中扩展它,并通过添加env.put(Context.SECURITY_PROTOCOL, "tls");覆盖bindAsUser() env.put(Context.SECURITY_PROTOCOL, "tls"); to the environment hashtable.到环境哈希表。 That's it.而已。

To control more closely the bind username, the domain, and the LDAP querystring, override the applicable methods as appropriate.要更密切地控制绑定用户名、域和 LDAP 查询字符串,请根据需要覆盖适用的方法。 In my case, I could not identify just what the value of {0} was, so I removed it entirely and inserted the passed username string instead.就我而言,我无法确定{0}的值是什么,所以我完全删除了它并插入了传递的username字符串。

Hopefully, someone out there finds this helpful.希望有人在那里发现这有帮助。

Alternatively if you don't mind using spring-ldap and creating a factory class under org.springframework.security.ldap.authentication.ad it is also possible to hack ActiveDirectoryLdapAuthenticationProvider by overriding contextFactory which is allowed for package protected access for testing purposes using following:或者,如果您不介意使用 spring-ldap 并在org.springframework.security.ldap.authentication.ad下创建工厂类,则也可以通过覆盖contextFactory来破解ActiveDirectoryLdapAuthenticationProvider ,该contextFactory允许使用以下方法进行包保护访问以进行测试:

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);
    }
}

Bonus content: If you don't want to deal with cert/naming issues which is usually the case for AD you can go with following instead:奖励内容:如果您不想处理 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: For the sake of code readability Lombok is used. PS:为了代码可读性,使用了 Lombok。 Naturally it is optional and can be easily removed.当然,它是可选的,并且可以轻松移除。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM