在之前的一篇文章中,讨论了如何使用原生工具,例如COM4J,来实现与Active Directory(AD)的连接。然而,这些工具使用起来可能相当繁琐,而使用纯Java则要简单得多。毕竟,还有什么能比调用Java内置包的几个简单函数更好、更简单的呢?
在实现单点登录(SSO)项目时,遇到了这个问题。在"原生"语言如C++或C#上实现AD上的SSO要直接得多。然而,在Java中,事情就更具挑战性了,花了相当长的时间才找到这个不需要任何本地工具如COM4J,或者JNI调用另一个C/C#程序的解决方案。
像Waffle这样的项目让生活变得容易多了。它实现了本地机器上的Windows与Active Directory之间的协商,从而执行SSO机制。然而,即使是Waffle也有其局限性。例如,尽管它确实负责认证,但它无法从AD检索所有所需的参数。使用Waffle来检索用户的电子邮件地址、电话号码、地址等是不可能的。
为了克服这个问题,一个选择是使用本地工具,如COM4J。COM4J确实效果很好,但它的缺点是它需要额外的理解并且有其陷阱。如果一切正常,每个人都很高兴,但一旦出现问题,就必须深入挖掘并解决问题,没有人真的想进入的角落。例如,使用COM4J迫使开发者将相关的JAR包含在构建路径中,或者担心安装在“web-inf/lib”目录中的COM4J.DLL版本(32/64?AMD?)等。
本文展示了如何使用Java实现这一目的,而不需要任何其他本地工具或任何其他依赖项。顺便说一句,一旦一切正常工作,如果想稍微提高性能,可以使用Spring forLDAP,但让把它留到最后。
将代码分为三部分。第一部分连接到AD。第二部分使用连接细节,如Context和SearchBase,并从AD获取想要的数据。最后一部分——好吧,这是使用前两部分的代码,展示它是如何工作的。使用Spring 3来声明bean为“Components”,并将这些bean自动装配到使用它们的类中。这里不讨论这个问题,假设读者知道如何使用Spring。
ActiveDirectoryConnectionUtils负责连接。解释如何使用Java的连接池不在本文范围内;有关LDAP连接池的更多信息,请阅读Oracle的“LDAP Connections”部分。
@Component
public class ActiveDirectoryConnectionUtils {
public LdapContext createContext(String url, String user, String pass) { Hashtable env = getProperties(url, user, pass);
LdapContext ctx;
try {
ctx = new InitialLdapContext(env, null);
} catch (NamingException e) {
throw new RuntimeException(e);
}
return ctx;
}
private Hashtable getProperties(String serverUrl, String user, String password) {
// create an initial directory context
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.REFERRAL, "ignore");
env.put("com.sun.jndi.ldap.connect.pool", "false");
// environment property to specify how long to wait for a pooled connection.
// If you omit this property, the application will wait indefinitely.
env.put("com.sun.jndi.ldap.connect.timeout", "300000");
env.put(Context.PROVIDER_URL, serverUrl);
env.put(Context.SECURITY_PRINCIPAL, user);
env.put(Context.SECURITY_CREDENTIALS, password);
env.put("java.naming.ldap.attributes.binary", "tokenGroups objectSid objectGUID");
return env;
}
}
这段代码基本上去AD,使用它从前面看到的类中得到的输入。首先,它从AD获取所有数据并将其存储在NamingEnumeration<SearchResult>中,这是通过过滤器搜索结果的枚举。然后,它在这个列表中搜索特定属性。在下面的代码中,这个属性是用户的电子邮件,通过属性“AD_ATTR_NAME_USER_EMAIL”来搜索它。当然,这个实现只是一个例子,并且可以根据一个客户端到另一个客户端而变化。
@Component
public class ActiveDirectoryLdapService {
private static Logger logger = Logger.getLogger(ActiveDirectoryLdapService.class);
// Attribute names
private static final String AD_ATTR_NAME_TOKEN_GROUPS = "tokenGroups";
private static final String AD_ATTR_NAME_OBJECT_CLASS = "objectClass";
private static final String AD_ATTR_NAME_OBJECT_CATEGORY = "objectCategory";
private static final String AD_ATTR_NAME_MEMBER = "member";
private static final String AD_ATTR_NAME_MEMBER_OF = "memberOf";
private static final String AD_ATTR_NAME_DESCRIPTION = "description";
private static final String AD_ATTR_NAME_OBJECT_GUID = "objectGUID";
private static final String AD_ATTR_NAME_OBJECT_SID = "objectSid";
private static final String AD_ATTR_NAME_DISTINGUISHED_NAME = "distinguishedName";
private static final String AD_ATTR_NAME_CN = "cn";
private static final String AD_ATTR_NAME_USER_PRINCIPAL_NAME = "userPrincipalName";
private static final String AD_ATTR_NAME_USER_EMAIL = "mail";
private static final String AD_ATTR_NAME_GROUP_TYPE = "groupType";
private static final String AD_ATTR_NAME_SAM_ACCOUNT_TYPE = "sAMAccountType";
private static final String AD_ATTR_NAME_USER_ACCOUNT_CONTROL = "userAccountControl";
/*
*
* @param ctx
* @param searchBase
* @param domainWithUser: suck as "MYDOMAIN\myUser"
* @return
*/
public String getUserMailByDomainWithUser(LdapContext ctx, String searchBase, String domainWithUser) {
logger.debug("trying to get email of domainWithUser " + domainWithUser + " using baseDN " + searchBase);
String userName = domainWithUser.substring(domainWithUser.indexOf('\\') +1);
try {
NamingEnumeration userDataBysAMAccountName = getUserDataBysAMAccountName(ctx, searchBase, userName);
return getUserMailFromSearchResults(userDataBysAMAccountName);
}
catch(Exception e) {
throw new RuntimeException(e);
}
}
private NamingEnumeration getUserDataBysAMAccountName(LdapContext ctx, String searchBase, String username) throws Exception {
String filter = "(&(&(objectClass=person)(objectCategory=user))(sAMAccountName=" + username + "))";
SearchControls searchCtls = new SearchControls();
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration answer = null;
try {
answer = ctx.search(searchBase, filter, searchCtls);
}
catch(Exception e) {
logger.error("Error searching Active directory for " + filter);
throw e;
}
return answer;
}
private String getUserMailFromSearchResults(NamingEnumeration userData) throws Exception {
try {
String mail = null;
// getting only the first result if we have more than one
if(userData.hasMoreElements()) {
SearchResult sr = userData.nextElement();
Attributes attributes = sr.getAttributes();
mail = attributes.get(AD_ATTR_NAME_USER_EMAIL).get().toString();
logger.debug("found email " + mail);
}
return mail;
}
catch(Exception e) {
logger.error("Error fetching attribute from object");
throw e;
}
}
}
要使用上面的代码,用户只需要调用两个方法:createContext(),然后在获取上下文后,getUserMailByDomainWithUser()。客户端应用程序必须提供以下内容:
在下面的例子中,只对用户的电子邮件感兴趣。上面的前三个参数是按系统配置的,因此它们是从属性文件中读取的。对于这个例子的目的,可以硬编码它们。唯一在运行时可更改的参数是要查找其电子邮件的用户的FQN。
FQN应该看起来像"john\doe",意味着域名是"john",用户名是"doe"。
public class LdapTester {
@Value("${com.watchdox.kerberos.ad.url}")
private String url;
@Value("${com.watchdox.kerberos.ad.username}")
private String username;
@Value("${com.watchdox.kerberos.ad.password}")
private String password;
@Value("${com.watchdox.kerberos.ad.baseDN}")
private String baseDN;
@Autowired
private ActiveDirectoryConnectionUtils adConnectionUtils;
@Autowired
private ActiveDirectoryLdapService adLdapService;
public void testGetUserMailByDomainWithUser(String fqn) {
LdapContext ctx = adConnectionUtils.createContext(url, username, password);
String email = adLdapService.getUserMailByDomainWithUser(ctx, baseDN, fqn);
}
}