使用Java连接Active Directory

在之前的一篇文章中,讨论了如何使用原生工具,例如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()。客户端应用程序必须提供以下内容:

  • LDAP服务器的URL
  • 该服务器的凭据(用户名和密码)
  • AD中的SearchBase路径的字符串
  • AD中用户的FQN(完全限定名)

在下面的例子中,只对用户的电子邮件感兴趣。上面的前三个参数是按系统配置的,因此它们是从属性文件中读取的。对于这个例子的目的,可以硬编码它们。唯一在运行时可更改的参数是要查找其电子邮件的用户的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); } }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485