使用 Java 的 LDAP 身份验证
Posted
技术标签:
【中文标题】使用 Java 的 LDAP 身份验证【英文标题】:LDAP Authentication using Java 【发布时间】:2012-09-01 06:49:13 【问题描述】:我需要为应用程序进行 LDAP 身份验证。
我尝试了以下程序:
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
public class LdapContextCreation
public static void main(String[] args)
LdapContextCreation ldapContxCrtn = new LdapContextCreation();
LdapContext ctx = ldapContxCrtn.getLdapContext();
public LdapContext getLdapContext()
LdapContext ctx = null;
try
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.SECURITY_AUTHENTICATION, "Simple");
//it can be <domain\\userid> something that you use for windows login
//it can also be
env.put(Context.SECURITY_PRINCIPAL, "username@domain.com");
env.put(Context.SECURITY_CREDENTIALS, "password");
//in following property we specify ldap protocol and connection url.
//generally the port is 389
env.put(Context.PROVIDER_URL, "ldap://server.domain.com");
ctx = new InitialLdapContext(env, null);
System.out.println("Connection Successful.");
catch(NamingException nex)
System.out.println("LDAP Connection: FAILED");
nex.printStackTrace();
return ctx;
得到以下异常:
LDAP 连接:失败 javax.naming.AuthenticationException:[LDAP:错误代码 49 - 无效凭据] 在 com.sun.jndi.ldap.LdapCtx.mapErrorCode(LdapCtx.java:3053) 在 com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:2999) 在 com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:2801) 在 com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2715) 在 com.sun.jndi.ldap.LdapCtx.(LdapCtx.java:305) 在 com.sun.jndi.ldap.LdapCtxFactory.getUsingURL(LdapCtxFactory.java:187) 在 com.sun.jndi.ldap.LdapCtxFactory.getUsingURLs(LdapCtxFactory.java:205) 在 com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxInstance(LdapCtxFactory.java:148) 在 com.sun.jndi.ldap.LdapCtxFactory.getInitialContext(LdapCtxFactory.java:78) 在 javax.naming.spi.NamingManager.getInitialContext(NamingManager.java:235) 在 javax.naming.InitialContext.initializeDefaultInitCtx(InitialContext.java:318) 在 javax.naming.InitialContext.getDefaultInitCtx(InitialContext.java:348) 在 javax.naming.InitialContext.internalInit(InitialContext.java:286) 在 javax.naming.InitialContext.init(InitialContext.java:308) 在 javax.naming.ldap.InitialLdapContext.(InitialLdapContext.java:99) 在 LdapContextCreation.getLdapContext(LdapContextCreation.java:27) 在 LdapContextCreation.main(LdapContextCreation.java:12)还有几点需要考虑:
之前我用的是tomcat 5.3.5
,但有人告诉我只有tomcat 6支持,所以我下载了tomcat 6.0.35
,目前只使用这个版本。
配置server.xml
并添加如下代码-
<Realm className="org.apache.catalina.realm.JNDIRealm"
debug="99"
connectionURL="ldap://server.domain.com:389/"
userPattern="0" />
从server.xml
注释了以下代码-
<!-- Commenting for LDAP
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/> -->
article 中的步骤 2 和 3
有人建议有一些 jar 文件应该复制到 tomcat 以运行ldap
身份验证,这是我需要做的吗?以及哪些jar
文件?
另外,我肯定使用了正确的凭据,那么是什么导致了这个问题?
如果我使用了不正确的属性,有没有办法找出正确的 LDAP 属性?
以下代码使用纯 Java JNDI 从 LDAP 进行身份验证。原则是:-
-
首先使用管理员或 DN 用户查找用户。
需要使用用户凭据再次将用户对象传递给 LDAP
无异常意味着 - 已成功验证。其他身份验证失败。
代码片段
public static boolean authenticateJndi(String username, String password) throws Exception
Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
props.put(Context.PROVIDER_URL, "ldap://LDAPSERVER:PORT");
props.put(Context.SECURITY_PRINCIPAL, "uid=adminuser,ou=special users,o=xx.com");//adminuser - User with special priviledge, dn user
props.put(Context.SECURITY_CREDENTIALS, "adminpassword");//dn user password
InitialDirContext context = new InitialDirContext(props);
SearchControls ctrls = new SearchControls();
ctrls.setReturningAttributes(new String[] "givenName", "sn","memberOf" );
ctrls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration<javax.naming.directory.SearchResult> answers = context.search("o=xx.com", "(uid=" + username + ")", ctrls);
javax.naming.directory.SearchResult result = answers.nextElement();
String user = result.getNameInNamespace();
try
props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
props.put(Context.PROVIDER_URL, "ldap://LDAPSERVER:PORT");
props.put(Context.SECURITY_PRINCIPAL, user);
props.put(Context.SECURITY_CREDENTIALS, password);
context = new InitialDirContext(props);
catch (Exception e)
return false;
return true;
【讨论】:
我知道这个条目有点老了,但我真的有一个迫切的问题:为什么我们需要一个管理员用户,为什么我们不能简单地尝试绑定我们想要的用户凭据首先进行身份验证? @Marci-man 这是为了简化要在您的 login() 方法中传递的参数。Context.SECURITY_PRINCIPAL
参数可能需要完整的 LDAP 对象地址,或某些特定的命名约定,这不容易键入。此外,用户对象可能会被重新组织(例如,用户被指定到公司内的另一个部门),因此 LDAP 路径可能会改变。
问题。在安全端口上,LDAP 连接在空闲一段时间后超时。我们如何防止它发生?一些 SO 帖子指向使用 ldap_abandon
和 LDAP_OPT_RECONNECT
但我不确定如何在此处设置这些值【参考方案2】:
这是我的 LDAP Java 登录测试应用程序,支持 LDAP:// 和 LDAPS:// 自签名测试证书。代码取自一些 SO 帖子,简化了实现并删除了遗留的 sun.java.* 导入。
how to accept self-signed certificates for JNDI/LDAP connections? Authenticating against Active Directory with Java on Linux用法 我已经在 Windows7 和 Linux 机器上针对 WinAD 目录服务运行了它。应用程序打印用户名和成员组。
$ java -cp classes test.LoginLDAP url=ldap://1.2.3.4:389 username=myname@company.fi 密码=mypwd
$ java -cp classes test.LoginLDAP url=ldaps://1.2.3.4:636 username=myname@company.fi 密码=mypwd
测试应用程序支持 ldaps:// 协议的临时自签名测试证书,这个 DummySSLFactory 接受任何服务器证书,因此中间人是可能的。实际安装应将服务器证书导入本地 JKS 密钥库文件,而不是使用虚拟工厂。
应用程序使用最终用户的用户名+密码进行初始上下文和 ldap 查询,它适用于 WinAD,但不知道是否可用于所有 ldap 服务器实现。您可以使用内部用户名+密码创建上下文,然后运行查询以查看是否找到给定的最终用户。
LoginLDAP.java
package test;
import java.util.*;
import javax.naming.*;
import javax.naming.directory.*;
public class LoginLDAP
public static void main(String[] args) throws Exception
Map<String,String> params = createParams(args);
String url = params.get("url"); // ldap://1.2.3.4:389 or ldaps://1.2.3.4:636
String principalName = params.get("username"); // firstname.lastname@mydomain.com
String domainName = params.get("domain"); // mydomain.com or empty
if (domainName==null || "".equals(domainName))
int delim = principalName.indexOf('@');
domainName = principalName.substring(delim+1);
Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
props.put(Context.PROVIDER_URL, url);
props.put(Context.SECURITY_PRINCIPAL, principalName);
props.put(Context.SECURITY_CREDENTIALS, params.get("password")); // secretpwd
if (url.toUpperCase().startsWith("LDAPS://"))
props.put(Context.SECURITY_PROTOCOL, "ssl");
props.put(Context.SECURITY_AUTHENTICATION, "simple");
props.put("java.naming.ldap.factory.socket", "test.DummySSLSocketFactory");
InitialDirContext context = new InitialDirContext(props);
try
SearchControls ctrls = new SearchControls();
ctrls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration<SearchResult> results = context.search(toDC(domainName),"(& (userPrincipalName="+principalName+")(objectClass=user))", ctrls);
if(!results.hasMore())
throw new AuthenticationException("Principal name not found");
SearchResult result = results.next();
System.out.println("distinguisedName: " + result.getNameInNamespace() ); // CN=Firstname Lastname,OU=Mycity,DC=mydomain,DC=com
Attribute memberOf = result.getAttributes().get("memberOf");
if(memberOf!=null)
for(int idx=0; idx<memberOf.size(); idx++)
System.out.println("memberOf: " + memberOf.get(idx).toString() ); // CN=Mygroup,CN=Users,DC=mydomain,DC=com
//Attribute att = context.getAttributes(memberOf.get(idx).toString(), new String[]"CN").get("CN");
//System.out.println( att.get().toString() ); // CN part of groupname
finally
try context.close(); catch(Exception ex)
/**
* Create "DC=sub,DC=mydomain,DC=com" string
* @param domainName sub.mydomain.com
* @return
*/
private static String toDC(String domainName)
StringBuilder buf = new StringBuilder();
for (String token : domainName.split("\\."))
if(token.length()==0) continue;
if(buf.length()>0) buf.append(",");
buf.append("DC=").append(token);
return buf.toString();
private static Map<String,String> createParams(String[] args)
Map<String,String> params = new HashMap<String,String>();
for(String str : args)
int delim = str.indexOf('=');
if (delim>0) params.put(str.substring(0, delim).trim(), str.substring(delim+1).trim());
else if (delim==0) params.put("", str.substring(1).trim());
else params.put(str, null);
return params;
还有 SSL 帮助类。
package test;
import java.io.*;
import java.net.*;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import javax.net.*;
import javax.net.ssl.*;
public class DummySSLSocketFactory extends SSLSocketFactory
private SSLSocketFactory socketFactory;
public DummySSLSocketFactory()
try
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[] new DummyTrustManager(), new SecureRandom());
socketFactory = ctx.getSocketFactory();
catch ( Exception ex ) throw new IllegalArgumentException(ex);
public static SocketFactory getDefault() return new DummySSLSocketFactory();
@Override public String[] getDefaultCipherSuites() return socketFactory.getDefaultCipherSuites();
@Override public String[] getSupportedCipherSuites() return socketFactory.getSupportedCipherSuites();
@Override public Socket createSocket(Socket socket, String string, int i, boolean bln) throws IOException
return socketFactory.createSocket(socket, string, i, bln);
@Override public Socket createSocket(String string, int i) throws IOException, UnknownHostException
return socketFactory.createSocket(string, i);
@Override public Socket createSocket(String string, int i, InetAddress ia, int i1) throws IOException, UnknownHostException
return socketFactory.createSocket(string, i, ia, i1);
@Override public Socket createSocket(InetAddress ia, int i) throws IOException
return socketFactory.createSocket(ia, i);
@Override public Socket createSocket(InetAddress ia, int i, InetAddress ia1, int i1) throws IOException
return socketFactory.createSocket(ia, i, ia1, i1);
class DummyTrustManager implements X509TrustManager
@Override public void checkClientTrusted(X509Certificate[] xcs, String str)
// do nothing
@Override public void checkServerTrusted(X509Certificate[] xcs, String str)
/*System.out.println("checkServerTrusted for authType: " + str); // RSA
for(int idx=0; idx<xcs.length; idx++)
X509Certificate cert = xcs[idx];
System.out.println("X500Principal: " + cert.getSubjectX500Principal().getName());
*/
@Override public X509Certificate[] getAcceptedIssuers()
return new java.security.cert.X509Certificate[0];
【讨论】:
【参考方案3】:您必须在SECURITY_PRINCIPAL
中提供整个用户dn
喜欢这个
env.put(Context.SECURITY_PRINCIPAL, "cn=username,ou=testOu,o=test");
【讨论】:
不正确,这取决于服务器实现。 @Michael-O 当然是这样。 (1) 它在 JNDI 规范中。 (2) LDAP 身份验证是通过 LDAP“绑定”操作在所有 LDAP 服务器实现上完成的。 (3) JNDI的作用是克服服务器实现的差异。 @EJP,我说的是仅将 DN 用于绑定过程。 @Jasim,我尝试了你的建议,但仍然没有运气:(。env.put(Context.SECURITY_PRINCIPAL, "cn=username@domain.com,ou=ouname,o=oname");
有没有办法找出正确的属性,以防我使用不正确的属性。
@Michael-O 在 LDAP 协议 RFC 4513 中指定在绑定过程中使用整个 DN。它不依赖于服务器。如果您知道例外情况,请提供。【参考方案4】:
// this class will authenticate LDAP UserName or Email
// simply call LdapAuth.authenticateUserAndGetInfo (username,password);
//Note: Configure ldapURI ,requiredAttributes ,ADSearchPaths,accountSuffex
import java.util.*;
import javax.naming.*;
import java.util.regex.*;
import javax.naming.directory.*;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
public class LdapAuth
private final static String ldapURI = "ldap://20.200.200.200:389/DC=corp,DC=local";
private final static String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
private static String[] requiredAttributes = "cn","givenName","sn","displayName","userPrincipalName","sAMAccountName","objectSid","userAccountControl";
// see you active directory user OU's hirarchy
private static String[] ADSearchPaths =
"OU=O365 Synced Accounts,OU=ALL USERS",
"OU=Users,OU=O365 Synced Accounts,OU=ALL USERS",
"OU=In-House,OU=Users,OU=O365 Synced Accounts,OU=ALL USERS",
"OU=Torbram Users,OU=Users,OU=O365 Synced Accounts,OU=ALL USERS",
"OU=Migrated Users,OU=TES-Users"
;
private static String accountSuffex = "@corp.local"; // this will be used if user name is just provided
private static void authenticateUserAndGetInfo (String user, String password) throws Exception
try
Hashtable<String,String> env = new Hashtable <String,String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory);
env.put(Context.PROVIDER_URL, ldapURI);
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, user);
env.put(Context.SECURITY_CREDENTIALS, password);
DirContext ctx = new InitialDirContext(env);
String filter = "(sAMAccountName="+user+")"; // default for search filter username
if(user.contains("@")) // if user name is a email then
//String parts[] = user.split("\\@");
//use different filter for email
filter = "(userPrincipalName="+user+")";
SearchControls ctrl = new SearchControls();
ctrl.setSearchScope(SearchControls.SUBTREE_SCOPE);
ctrl.setReturningAttributes(requiredAttributes);
NamingEnumeration userInfo = null;
Integer i = 0;
do
userInfo = ctx.search(ADSearchPaths[i], filter, ctrl);
i++;
while(!userInfo.hasMore() && i < ADSearchPaths.length );
if (userInfo.hasMore())
SearchResult UserDetails = (SearchResult) userInfo.next();
Attributes userAttr = UserDetails.getAttributes();System.out.println("adEmail = "+userAttr.get("userPrincipalName").get(0).toString());
System.out.println("adFirstName = "+userAttr.get("givenName").get(0).toString());
System.out.println("adLastName = "+userAttr.get("sn").get(0).toString());
System.out.println("name = "+userAttr.get("cn").get(0).toString());
System.out.println("AdFullName = "+userAttr.get("cn").get(0).toString());
userInfo.close();
catch (javax.naming.AuthenticationException e)
【讨论】:
以上是关于使用 Java 的 LDAP 身份验证的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 xml 配置文件、JAVA、Spring 安全性对 LDAP 用户进行身份验证