我有一个可用的概念验证应用程序,它可以在测试服务器上通过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
)?
感谢任何帮助。
答案 0 :(得分:6)
好的,经过大约一天半的工作后,我明白了。
我最初的方法是扩展Spring的ActiveDirectoryLdapAuthenticationProvider
类,并覆盖其loadUserAuthorities()
方法,以便自定义经过身份验证的用户的权限的构建方式。由于不明显的原因,ActiveDirectoryLdapAuthenticationProvider
类被指定为final
,所以当然我无法扩展它。
值得庆幸的是,开源提供了黑客攻击(该类的超类不是 final
),所以我只是复制了它的全部内容,删除了final
指定,并相应地调整了包和类引用。我没有编辑这个类中的任何代码,除了添加一个高度可见的注释,表示不编辑它。然后我在OverrideActiveDirectoryLdapAuthenticationProvider
中扩展了这个类,我在ldap.xml
文件中也引用了它,并在其中为loadUserAuthorities
添加了覆盖方法。所有这些都可以通过未加密的会话(在隔离的虚拟服务器上)进行简单的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"/>
然后我更改了searchForUser()
中的OverrideActiveDirectoryLdapAuthenticationProvider
方法:
@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()
方法,正确构建String(为了我的目的):
@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;博士强>:
要启用TLS,请复制Spring的ActiveDirectoryLdapAuthenticationProvider
类,删除final
标识,将其扩展到自定义类中,并通过将bindAsUser()
添加到环境哈希表来覆盖env.put(Context.SECURITY_PROTOCOL, "tls");
。就是这样。
要更严密地控制绑定用户名,域和LDAP查询字符串,请根据需要覆盖适用的方法。在我的情况下,我无法确定{0}
的值是什么,所以我完全删除它并插入传递的username
字符串。
希望有人能发现这有用。
答案 1 :(得分:0)
或者,如果您不介意使用 spring-ldap 并在 org.springframework.security.ldap.authentication.ad
下创建工厂类,也可以通过覆盖允许包保护访问的 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);
}
}
奖励内容:如果您不想处理 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。当然,它是可选的,可以轻松删除。