[英]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
:然后我更改了
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});
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.