65.9K
CodeProject 正在变化。 阅读更多。
Home

Java:从 Active Directory 检索用户信息的示例(例如电子邮件地址)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2013 年 9 月 15 日

CPOL

4分钟阅读

viewsIcon

100412

本文档介绍如何从 Active Directory 获取扩展用户数据,例如电子邮件地址。一篇之前的文章介绍了如何使用 COM4J 等原生工具执行此操作;然而,这些工具使用起来很麻烦,而纯 Java 的方法更好、更简单。

引言

本文档介绍如何从 Active Directory (AD) 获取扩展的用户数据,例如电子邮件地址。 一篇之前的文章介绍了如何使用 COM4J 等原生工具执行此操作。然而,这些工具使用起来可能很麻烦,而使用纯 Java 则要简单得多,因此也更好:毕竟,还有什么比调用几个简单的内置 Java 包更方便、更容易的呢?

动机

在实现 SSO(单点登录)项目时,我不得不处理这个问题。

在 C++ 甚至 C# 等“原生”语言中,在 AD 上实现 SSO 要直接得多。然而,在 Java 中,事情更具挑战性,花了很长时间才找到这个解决方案,它不需要任何原生工具(如 COM4J)或与其他 C/C# 程序的 JNI 调用。

Waffle 这样的项目让生活变得更容易。它实现了本地机器上的 Windows 与 Active Directory 之间的协商,从而执行 SSO 机制。然而,即使是 Waffle——尽管在很多方面都有帮助——也有其局限性。例如,即使它处理了身份验证,也无法从 AD 检索所有期望的参数。使用 Waffle 检索用户的电子邮件地址、电话号码、地址等是不可能的。

为了解决这个问题,一个选择是使用原生工具,例如 COM4J。COM4J 的确运行良好,但其缺点是需要额外的理解并存在陷阱。如果一切正常,每个人都会很高兴,但一旦出现问题,就必须深入挖掘并解决那些没人真正想涉足的角落。例如,使用 COM4J 会强制开发人员将相关 JAR 添加到构建路径中,或者担心安装在“web-inf/lib”目录中的COM4J.DLL 版本(32/64?AMD?),等等。

本文档将介绍如何仅使用 Java 来完成此任务,而无需任何其他原生工具或任何其他依赖项。顺便说一句,我想提一下,一旦一切正常,并且您想稍微提高一下性能,可以使用 Spring for LDAP,但我们留到最后再说。

代码

我将代码分为三个部分。第一部分连接到 AD。第二部分使用连接详细信息(如 ContextSearchBase)从 AD 获取我们想要的数据。最后一部分——嗯,这是使用前两部分的代码,展示了它是如何工作的。我使用 Spring 3 将我们的 bean 声明为“Components”,并将这些 bean autowire 到使用它们的类中。这方面的详细讨论超出了本文档的范围,我假设读者了解如何使用 Spring。

ActiveDirectoryConnectionUtils

ActiveDirectoryConnectionUtils 负责连接。解释如何使用 Java 的连接池超出了本文档的范围;有关 LDAP 连接池的更多信息,请阅读 Oracle 的“LDAP 连接”部分

@Component
public class ActiveDirectoryConnectionUtils
{
    public LdapContext createContext(String url, String user, String pass)
    {    	        Hashtable<String,String> env = getProperties(url, user, pass);
        LdapContext ctx;
        try
        {
	        ctx = new InitialLdapContext(env, null);
        }
        catch (NamingException e)
        {
	        throw new RuntimeException(e);
        }
        return ctx;
    }
    private Hashtable<String,String> getProperties(String serverUrl, String user, String password)
    {
        //create an initial directory context
        Hashtable<String,String> env = new Hashtable<String,String>();
        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;
    }
}

ActiveDirectoryLdapService

这段代码基本上会连接到 AD,使用我们刚刚看到的上一个类提供的输入。首先,它从 AD 获取所有数据并将其存储在 NamingEnumeration<SearchResult> 中,这是一个按过滤器搜索结果的枚举。然后,它在该列表中搜索特定属性。在下面的代码中,此属性是用户的电子邮件,我们通过属性“AD_ATTR_NAME_USER_EMAIL”进行搜索。当然,这个实现只是一个示例,并且可能因客户而异。

要了解有关 LDAP 过滤器的更多信息,请在此处阅读

@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<SearchResult> 
	    	userDataBysAMAccountName = getUserDataBysAMAccountName(ctx, searchBase, userName);
	    	return getUserMailFromSearchResults( userDataBysAMAccountName );
		}
		catch(Exception e)
		{
			throw new RuntimeException(e);
		}
	}
		private NamingEnumeration<SearchResult> 
		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<SearchResult> 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<SearchResult> 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
  • 该服务器的凭据(用户名和密码)
  • 一个 String,即 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);
	}
}

性能

上面的代码会在每次访问 AD 时打开一个连接,这可能会导致性能问题。有几种方法可以解决这个问题。在我看来,最容易实现的是连接池,可以使用 Spring-LDAP 包来实现;这方面的细节超出了本文档的范围。

致谢

我的同事 Shalom Kazaz 和 Or Gerson,以及特别感谢 David Goldhar 先生。

© . All rights reserved.