1. Introduction
1.介绍
In this article, we’ll cover how to authenticate a user with LDAP using pure Java. Furthermore, we’ll explore how to search for a user’s distinguished name (DN). This is important because LDAP requires the DN to authenticate the user.
在本文中,我们将介绍如何用LDAP的纯Java来验证用户。此外,我们还将探讨如何搜索用户的区分名称(DN)。这一点很重要,因为LDAP需要DN来验证用户的身份。
To do the search and user authentication, we’ll use the directory service access capabilities of the Java Naming and Directory Interface (JNDI).
为了进行搜索和用户认证,我们将使用Java命名和目录接口(JNDI>)的目录服务访问功能。
First, we’ll briefly discuss what LDAP and JNDI are. Then we’ll discuss how to authenticate with LDAP through the JNDI API.
首先,我们将简要地讨论什么是LDAP和JNDI。然后我们将讨论如何通过JNDI API用LDAP进行认证。
2. What Is LDAP?
2.什么是LDAP?
The Lightweight Directory Access Protocol (LDAP) defines a way for clients to send requests and receive responses from directory services. We call a directory service using this protocol an LDAP server.
轻量级目录访问协议(LDAP)定义了一种客户发送请求并从目录服务接收响应的方式。我们将使用该协议的目录服务称为LDAP服务器。
The data served by an LDAP server is stored in an information model based on X.500. This is a group of computer networking standards for electronic directory services.
由LDAP服务器提供的数据存储在一个基于X.500的信息模型中。这是一组用于电子目录服务的计算机网络标准。
3. What Is JNDI?
3.什么是JNDI??
JNDI provides a standard API for applications to discover and access naming and directory services. Its fundamental purpose is to provide a way for applications to access components and resources. This being, both locally and over a network.
JNDI为应用程序提供了一个标准的API,用于发现和访问命名和目录服务。其基本目的是为应用程序提供一种访问组件和资源的方式。这一点,无论是本地还是通过网络。
Naming services underly this capability because they provide single-point access to services, data, or objects by name in a hierarchical namespace. The name given to each of these local or network-accessible resources is configured on the server hosting the naming service.
命名服务是这种能力的基础,因为它们提供了对服务、数据或对象的单点访问,在一个分层的命名空间中通过名称进行访问。给予这些本地或网络可访问的资源的名称是在托管命名服务的服务器上配置的。
We can access directory services, like LDAP, by using the naming service interface of JNDI. This is because a directory service is merely a specialized type of naming service that enables each named entry to have a list of attributes associated with it.
我们可以通过使用JNDI的命名服务接口访问目录服务,如LDAP。这是因为目录服务只是一种专门的命名服务类型,它使每个命名的条目都有一个与之相关的属性列表。
Besides attributes, each directory entry may have one or more children. This enables entries to be linked hierarchically. In JNDI, children of directory entries are represented as subcontexts of their parent context.
除了属性,每个目录条目可以有一个或多个子项。这使得条目可以被分层链接。在JNDI中,目录项的子项被表示为其父项的子上下文。
A key benefit of the JNDI API is that it’s independent of any underlying service provider implementation, for example, LDAP. Therefore, we can use JNDI to access an LDAP directory service without needing to use protocol-specific APIs.
JNDI API的一个关键好处是,它独立于任何底层服务提供商的实现,例如LDAP。因此,我们可以使用JNDI来访问LDAP目录服务,而不需要使用特定协议的API。
No external libraries are needed to use JNDI because it’s part of the Java SE platform. Further, being a core technology in Java EE, it is widely used to implement enterprise applications.
使用JNDI不需要外部库,因为它是Java SE平台的一部分。此外,作为Java EE的一项核心技术,它被广泛用于实现企业应用。
4. JNDI API Concepts to Authenticate with LDAP
4.使用LDAP进行认证的JNDI API概念
Before discussing the example code, let’s cover some fundamentals about using the JNDI API for LDAP-based authentication.
在讨论示例代码之前,让我们介绍一下关于使用JNDI API进行基于LDAP的认证的一些基本知识。
To connect to an LDAP server, we first need to create a JNDI InitialDirContext object. When doing so, we need to pass environment properties into its constructor as a Hashtable to configure it.
为了连接到LDAP服务器,我们首先需要创建一个JNDI InitialDirContext对象。这样做时,我们需要将环境属性作为Hashtable传入其构造函数,以配置它。
Amongst others, we need to add properties to this Hashtable for the user name and password that we wish to authenticate with. To do so, we must set the user’s DN and his password to the Context.SECURITY_PRINCIPAL and Context.SECURITY_CREDENTIALS properties, respectively.
其中,我们需要为这个Hashtable添加属性,用于我们希望用其进行验证的用户名和密码。为此,我们必须将用户的 DN 和他的密码分别设置为 Context.SECURITY_PRINCIPAL 和 Context.SECURITY_CREDENTIALS 属性。
InitialDirContext implements DirContext, the main JNDI directory service interface. Through this interface, we can use our new context to perform various directory service operations on the LDAP server. These include binding names and attributes to objects and searching for directory entries.
InitialDirContext实现了DirContext,这是一个主要的JNDI目录服务接口。通过这个接口,我们可以使用我们的新上下文来在LDAP服务器上执行各种目录服务操作。这些操作包括将名称和属性绑定到对象以及搜索目录条目。
It’s worth noting that the objects returned by JNDI will have the same names and attributes as their underlying LDAP entries. Thus, to search for an entry, we can use its name and attributes as criteria to look it up.
值得注意的是,由JNDI返回的对象将具有与其基础LDAP条目相同的名称和属性。因此,要搜索一个条目,我们可以用它的名字和属性作为标准来查找。
Once we have retrieved a directory entry through JNDI, we can look at its attributes using the Attributes interface. Further, we can use the Attribute interface to inspect each of them.
一旦我们通过JNDI检索到一个目录条目,我们就可以使用Attributes接口来查看其属性。此外,我们还可以使用Attribute接口来检查它们中的每一个。
5. What If We Don’t Have the User’s DN?
5.如果我们没有用户的DN,怎么办?
Sometimes we don’t have the DN of the user immediately available to authenticate with. To get around this, we first need to create an InitialDirContext using administrator credentials. After that, we can use it to search for the relevant user from the directory server and get his DN.
有时,我们没有用户的DN可以立即用来验证。为了解决这个问题,我们首先需要使用管理员凭证创建一个InitialDirContext。之后,我们可以用它来从目录服务器上搜索相关的用户,并获得他的DN。
Then once we have the user’s DN, we can authenticate him by creating a new InitialDirContext, this time with his credentials. To do this, we first need to set the user’s DN and password in the environment properties. After that, we need to pass these properties into the InitDirContext‘s constructor when creating it.
一旦我们有了用户的DN,我们就可以通过创建一个新的InitialDirContext来验证他,这次是用他的凭证。要做到这一点,我们首先需要在环境属性中设置用户的DN和密码。之后,我们需要在创建InitDirContext的构造函数中传递这些属性。
Now that we’ve discussed authenticating a user through LDAP using the JNDI API, let’s go through the example code.
现在我们已经讨论了使用JNDI API通过LDAP验证用户的问题,让我们来看看示例代码。
6. Example Code
6.代码示例
In our example, we’ll use the embedded version of the ApacheDS directory server. This is an LDAP server built using Java and designed to run in embedded mode within unit tests.
在我们的例子中,我们将使用ApacheDS目录服务器的嵌入式版本。这是一个使用 Java 构建的 LDAP 服务器,设计用于在单元测试中以嵌入式模式运行。
Let’s look at how to set it up.
让我们来看看如何设置它。
6.1. Setting up the Embedded ApacheDS Server
6.1.设置嵌入式ApacheDS服务器
To use the embedded ApacheDS server, we need to define the Maven dependency:
要使用嵌入式ApacheDS服务器,我们需要定义Maven 依赖。
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-test-framework</artifactId>
<version>2.0.0.AM26</version>
<scope>test</scope>
</dependency>
Next, we need to create a unit test class using JUnit 4. To use the embedded ApacheDS server in this class, we must declare that it extends AbstractLdapTestUnit from the ApacheDS library. As this library is not compatible with JUnit 5 yet, we need to use JUnit 4.
接下来,我们需要使用JUnit 4创建一个单元测试类。为了在这个类中使用嵌入式ApacheDS服务器,我们必须声明它扩展了ApacheDS库中的AbstractLdapTestUnit。由于这个库与JUnit 5还不兼容,我们需要使用JUnit 4。
Additionally, we need to include Java annotations above our unit test class declaration to configure the server. We can see which values to give them from the full code example, which we will explore later.
此外,我们需要在我们的单元测试类声明上面包括Java注解,以配置服务器。我们可以从完整的代码示例中看到要给它们哪些值,我们将在后面探讨。
Lastly, we’ll also need to add users.ldif to the classpath. This is so the ApacheDS server can load LDIF formatted directory entries from this file when we run our code example. When doing so, the server will load the entry for the user Joe Simms.
最后,我们还需要将users.ldif添加到classpath中。这样,当我们运行我们的代码示例时,ApacheDS服务器可以从这个文件中加载LDIF格式的目录条目。这样做时,服务器将加载用户Joe Simms的条目。
Next, we’ll discuss the example code that will authenticate the user. To run it against the LDAP server, we’ll need to add our code to a method in our unit test class. This will authenticate Joe through LDAP using his DN and password, as defined in the file.
接下来,我们将讨论验证用户的示例代码。为了针对LDAP服务器运行它,我们需要将我们的代码添加到我们单元测试类的一个方法中。这将通过LDAP认证Joe,使用他的DN和密码,如文件中定义的那样。
6.2. Authenticating the User
6.2.认证用户
To authenticate the user, Joe Simms, we need to create a new InitialDirContext object. This creates a connection to the directory server and authenticates the user through LDAP using his DN and password.
为了验证用户Joe Simms,我们需要创建一个新的InitialDirContext对象。这将创建一个与目录服务器的连接,并通过LDAP使用他的DN和密码对用户进行认证。
To do this, we first need to add these environment properties into a Hashtable:
要做到这一点,我们首先需要将这些环境属性添加到一个Hashtable中。
Hashtable<String, String> environment = new Hashtable<String, String>();
environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
environment.put(Context.PROVIDER_URL, "ldap://localhost:10389");
environment.put(Context.SECURITY_AUTHENTICATION, "simple");
environment.put(Context.SECURITY_PRINCIPAL, "cn=Joe Simms,ou=Users,dc=baeldung,dc=com");
environment.put(Context.SECURITY_CREDENTIALS, "12345");
Next, inside a new method called authenticateUser, we’ll create the InitialDirContext object by passing the environment properties into its constructor. Then, we’ll close the context to free up resources:
接下来,在一个名为authenticateUser的新方法中,我们将通过向其构造函数传递环境属性来创建InitialDirContext对象。然后,我们将关闭该上下文以释放资源。
DirContext context = new InitialDirContext(environment);
context.close();
Lastly, we’ll authenticate the user:
最后,我们将对用户进行认证。
assertThatCode(() -> authenticateUser(environment)).doesNotThrowAnyException();
Now that we’ve covered the case where user authentication succeeds, let’s examine when it fails.
现在我们已经涵盖了用户认证成功的情况,让我们来看看它失败的情况。
6.3. Handling User Authentication Failure
6.3.处理用户认证失败
Applying the same environment properties as previously, let’s make the authentication fail by using the wrong password:
应用与之前相同的环境属性,让我们通过使用错误的密码使认证失败。
environment.put(Context.SECURITY_CREDENTIALS, "wrongpassword");
Then, we’ll check that authenticating the user with this password fails as expected:
然后,我们将检查用这个密码认证用户是否像预期的那样失败。
assertThatExceptionOfType(AuthenticationException.class).isThrownBy(() -> authenticateUser(environment));
Next, let’s discuss how to authenticate the user when we don’t have his DN.
接下来,让我们讨论一下,当我们没有用户的DN时,如何对用户进行认证。
6.4. Looking up the User’s DN as Administrator
6.4.以管理员身份查询用户的DN
Sometimes when we want to authenticate a user, we don’t have his DN immediately at hand. In this situation, we first need to create a directory context with administrator credentials to look up the user’s DN and then authenticate the user with that.
有时,当我们想验证一个用户时,我们手头并没有他的DN。在这种情况下,我们首先需要用管理员证书创建一个目录上下文来查询用户的DN,然后用它来验证用户。
As before, we first need to add some environment properties in a Hashtable. But this time, we’ll use the administrator’s DN as the Context.SECURITY_PRINCIPAL, together with his default admin password as the Context.SECURITY_CREDENTIALS property:
和以前一样,我们首先需要在Hashtable中添加一些环境属性。但是这一次,我们将使用管理员的 DN 作为 Context.SECURITY_PRINCIPAL,以及他的默认管理密码作为 Context.SECURITY_CREDENTIALS 属性。
Hashtable<String, String> environment = new Hashtable<String, String>();
environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
environment.put(Context.PROVIDER_URL, "ldap://localhost:10389");
environment.put(Context.SECURITY_AUTHENTICATION, "simple");
environment.put(Context.SECURITY_PRINCIPAL, "uid=admin,ou=system");
environment.put(Context.SECURITY_CREDENTIALS, "secret");
Next, we’ll create an InitialDirContext object with those properties:
接下来,我们将用这些属性创建一个InitialDirContext对象。
DirContext adminContext = new InitialDirContext(environment);
This will create a directory context, with the connection to the server authenticated as the administrator. This gives us security rights to search for the user’s DN.
这将创建一个目录上下文,与服务器的连接被认证为管理员。这给了我们搜索用户DN的安全权利。
Now we’ll define the filter for our search based on the user’s CN, i.e., his common name.
现在我们将根据用户的CN,即他的常用名来定义搜索的过滤器。
String filter = "(&(objectClass=person)(cn=Joe Simms))";
Then, using this filter to search for the user, we’ll create a SearchControls object:
然后,使用这个过滤器来搜索用户,我们将创建一个SearchControls对象。
String[] attrIDs = { "cn" };
SearchControls searchControls = new SearchControls();
searchControls.setReturningAttributes(attrIDs);
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
Next, we’ll search for the user with our filter and SearchControls:
接下来,我们将用我们的filter和SearchControls来搜索用户。
NamingEnumeration<SearchResult> searchResults
= adminContext.search("dc=baeldung,dc=com", filter, searchControls);
String commonName = null;
String distinguishedName = null;
if (searchResults.hasMore()) {
SearchResult result = (SearchResult) searchResults.next();
Attributes attrs = result.getAttributes();
distinguishedName = result.getNameInNamespace();
assertThat(distinguishedName, isEqualTo("cn=Joe Simms,ou=Users,dc=baeldung,dc=com")));
commonName = attrs.get("cn").toString();
assertThat(commonName, isEqualTo("cn: Joe Simms")));
}
Let’s authenticate the user now that we have his DN.
现在我们来验证这个用户,因为我们有他的DN。
6.5. Authenticating with the User’s Looked up DN
6.5.使用用户查询的 DN 进行认证
With the user’s DN now at hand to authenticate with, we’ll replace the administrator’s DN and password in the existing environment properties with the user’s ones:
现在有了用户的DN来验证,我们将用用户的DN和密码来替换现有环境属性中的管理员的DN和密码。
environment.put(Context.SECURITY_PRINCIPAL, distinguishedName);
environment.put(Context.SECURITY_CREDENTIALS, "12345");
Then, with these in place, let’s authenticate the user:
然后,有了这些,让我们来验证用户的身份。
assertThatCode(() -> authenticateUser(environment)).doesNotThrowAnyException();
Lastly, we’ll close the administrator’s context to free up resources:
最后,我们将关闭管理员的上下文以释放资源。
adminContext.close();
7. Conclusion
7.结论
In this article, we discussed how to use JNDI to authenticate a user with LDAP utilizing the user’s DN and password.
在这篇文章中,我们讨论了如何使用JNDI通过LDAP利用用户的DN和密码来认证用户。
Also, we examined how to look up the DN if we don’t have it.
此外,我们还研究了在没有DN的情况下如何查询DN的问题。
As usual, the complete source code for the examples can be found over on GitHub.
像往常一样,这些例子的完整源代码可以在GitHub上找到。