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





5.00/5 (7投票s)
本文档介绍如何从 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。第二部分使用连接详细信息(如 Context
和 SearchBase
)从 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 先生。