Перейти к основному содержимому

Аутентификация LDAP с использованием чистой Java

· 8 мин. чтения

1. Введение

В этой статье мы расскажем, как аутентифицировать пользователя с помощью LDAP , используя чистую Java. Кроме того, мы рассмотрим, как искать отличительное имя пользователя (DN). Это важно, поскольку LDAP требует DN для аутентификации пользователя.

Чтобы выполнить поиск и аутентификацию пользователей, мы будем использовать возможности доступа к службе каталогов Java Naming and Directory Interface ( JNDI ).

Сначала мы кратко обсудим, что такое LDAP и JNDI. Затем мы обсудим, как пройти аутентификацию с помощью LDAP через JNDI API.

2. Что такое LDAP?

Облегченный протокол доступа к каталогам (LDAP) определяет способ отправки клиентами запросов и получения ответов от служб каталогов . Мы называем службу каталогов, использующую этот протокол, сервером LDAP.

Данные, обслуживаемые сервером LDAP, хранятся в информационной модели на основе X.500 . Это группа стандартов компьютерных сетей для служб электронных каталогов.

3. Что такое JNDI?

JNDI предоставляет приложениям стандартный API для обнаружения и доступа к службам имен и каталогов. Его основная цель — предоставить приложениям способ доступа к компонентам и ресурсам. Это существо, как локально, так и по сети.

Службы именования лежат в основе этой возможности, поскольку они обеспечивают одноточечный доступ к службам, данным или объектам по имени в иерархическом пространстве имен. Имя, данное каждому из этих локальных или сетевых ресурсов, настраивается на сервере, на котором размещена служба именования.

Мы можем получить доступ к службам каталогов, таким как LDAP, используя интерфейс службы имен JNDI . Это связано с тем, что служба каталогов является просто специализированным типом службы именования, которая позволяет каждой именованной записи иметь список связанных с ней атрибутов.

Помимо атрибутов, у каждой записи каталога может быть один или несколько дочерних элементов. Это позволяет иерархически связывать записи. В JNDI дочерние элементы записей каталога представлены как подконтексты их родительского контекста.

Ключевым преимуществом JNDI API является то, что он не зависит от какой-либо базовой реализации поставщика услуг, например LDAP. Таким образом, мы можем использовать JNDI для доступа к службе каталогов LDAP без необходимости использования специфичных для протокола API.

Для использования JNDI не требуются внешние библиотеки, поскольку это часть платформы Java SE. Кроме того, будучи базовой технологией Java EE, она широко используется для реализации корпоративных приложений.

4. Концепции JNDI API для аутентификации с помощью LDAP

Прежде чем обсуждать пример кода, давайте рассмотрим некоторые основы использования JNDI API для аутентификации на основе LDAP.

Чтобы подключиться к серверу LDAP, нам сначала нужно создать объект JNDI InitialDirContext . При этом нам нужно передать свойства среды в его конструктор в виде Hashtable для его настройки.

Среди прочего, нам нужно добавить в эту Hashtable свойства для имени пользователя и пароля, с которыми мы хотим пройти аутентификацию. Для этого мы должны установить DN пользователя и его пароль в свойствах Context.SECURITY_PRINCIPAL и Context.SECURITY_CREDENTIALS соответственно.

InitialDirContext реализует DirContext , основной интерфейс службы каталогов JNDI. Через этот интерфейс мы можем использовать наш новый контекст для выполнения различных операций службы каталогов на сервере LDAP. К ним относятся привязка имен и атрибутов к объектам и поиск записей каталога.

Стоит отметить, что объекты, возвращаемые JNDI, будут иметь те же имена и атрибуты, что и их базовые записи LDAP. Таким образом, для поиска записи мы можем использовать ее имя и атрибуты в качестве критериев для ее поиска.

Как только мы получили запись каталога через JNDI, мы можем просмотреть ее атрибуты, используя интерфейс Attributes . Кроме того, мы можем использовать интерфейс Attribute для проверки каждого из них.

5. Что делать, если у нас нет DN пользователя?

Иногда у нас нет DN пользователя, доступного для аутентификации. Чтобы обойти это, нам сначала нужно создать InitialDirContext , используя учетные данные администратора. После этого мы можем использовать его для поиска соответствующего пользователя на сервере каталогов и получения его DN.

Затем, когда у нас есть DN пользователя, мы можем аутентифицировать его, создав новый InitialDirContext , на этот раз с его учетными данными. Для этого нам сначала нужно установить DN пользователя и пароль в свойствах среды. После этого нам нужно передать эти свойства в конструктор InitDirContext при его создании.

Теперь, когда мы обсудили аутентификацию пользователя через LDAP с использованием JNDI API, давайте рассмотрим пример кода.

6. Пример кода

В нашем примере мы будем использовать встроенную версию сервера каталогов ApacheDS . Это сервер LDAP, созданный с использованием Java и предназначенный для работы во встроенном режиме в рамках модульных тестов.

Давайте посмотрим, как это настроить.

6.1. Настройка встроенного сервера ApacheDS

Чтобы использовать встроенный сервер ApacheDS, нам нужно определить зависимость Maven :

<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-test-framework</artifactId>
<version>2.0.0.AM26</version>
<scope>test</scope>
</dependency>

Далее нам нужно создать класс модульного теста с использованием JUnit 4. Чтобы использовать встроенный сервер ApacheDS в этом классе, мы должны объявить, что он расширяет AbstractLdapTestUnit из библиотеки ApacheDS. Поскольку эта библиотека еще не совместима с JUnit 5, нам нужно использовать JUnit 4.

Кроме того, нам нужно включить аннотации Java над объявлением класса модульного теста для настройки сервера. Мы можем увидеть, какие значения им присвоить, из примера полного кода , который мы рассмотрим позже.

Наконец, нам также нужно добавить файл users.ldif в путь к классам. Это сделано для того, чтобы сервер ApacheDS мог загружать записи каталога в формате LDIF из этого файла, когда мы запускаем наш пример кода. При этом сервер загрузит запись для пользователя Joe Simms .

Далее мы обсудим пример кода, который будет аутентифицировать пользователя. Чтобы запустить его на сервере LDAP, нам нужно добавить наш код в метод в нашем классе модульного теста. Это аутентифицирует Джо через LDAP, используя его DN и пароль, как указано в файле.

6.2. Аутентификация пользователя

Чтобы аутентифицировать пользователя Joe Simms , нам нужно создать новый объект InitialDirContext . Это создает соединение с сервером каталогов и аутентифицирует пользователя через LDAP, используя его DN и пароль.

Для этого нам сначала нужно добавить эти свойства среды в 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=foreach,dc=com");
environment.put(Context.SECURITY_CREDENTIALS, "12345");

Далее внутри нового метода с именем authenticationUser мы создадим объект InitialDirContext , передав свойства среды в его конструктор. Затем мы закроем контекст, чтобы освободить ресурсы:

DirContext context = new InitialDirContext(environment);
context.close();

Наконец, мы аутентифицируем пользователя:

assertThatCode(() -> authenticateUser(environment)).doesNotThrowAnyException();

Теперь, когда мы рассмотрели случай успешной аутентификации пользователя, давайте рассмотрим случаи, когда она терпит неудачу.

6.3. Обработка сбоя аутентификации пользователя

Применив те же свойства среды, что и ранее, давайте сделаем аутентификацию неудачной, используя неверный пароль:

environment.put(Context.SECURITY_CREDENTIALS, "wrongpassword");

Затем мы проверим, что аутентификация пользователя с этим паролем завершилась неудачно:

assertThatExceptionOfType(AuthenticationException.class).isThrownBy(() -> authenticateUser(environment));

Далее давайте обсудим, как аутентифицировать пользователя, когда у нас нет его DN.

6.4. Поиск DN пользователя в качестве администратора

Иногда, когда мы хотим аутентифицировать пользователя, у нас нет под рукой его DN. В этой ситуации нам сначала нужно создать контекст каталога с учетными данными администратора, чтобы найти DN пользователя, а затем аутентифицировать пользователя с помощью этого.

Как и раньше, сначала нам нужно добавить некоторые свойства среды в 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");

Далее мы создадим объект InitialDirContext со следующими свойствами:

DirContext adminContext = new InitialDirContext(environment);

Это создаст контекст каталога с подключением к серверу, аутентифицированным как администратор. Это дает нам права безопасности на поиск DN пользователя.

Теперь мы определим фильтр для нашего поиска на основе CN пользователя, т. е. его общего имени .

String filter = "(&(objectClass=person)(cn=Joe Simms))";

Затем, используя этот фильтр для поиска пользователя, мы создадим объект SearchControls :

String[] attrIDs = { "cn" };
SearchControls searchControls = new SearchControls();
searchControls.setReturningAttributes(attrIDs);
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);

Далее мы будем искать пользователя с помощью нашего фильтра и SearchControls :

NamingEnumeration<SearchResult> searchResults
= adminContext.search("dc=foreach,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=foreach,dc=com")));

commonName = attrs.get("cn").toString();
assertThat(commonName, isEqualTo("cn: Joe Simms")));
}

Давайте аутентифицируем пользователя теперь, когда у нас есть его DN.

6.5. Аутентификация с помощью поискового DN пользователя

Имея DN пользователя для аутентификации, мы заменим DN и пароль администратора в существующих свойствах среды на пользовательские:

environment.put(Context.SECURITY_PRINCIPAL, distinguishedName);
environment.put(Context.SECURITY_CREDENTIALS, "12345");

Затем, с этим на месте, давайте аутентифицируем пользователя:

assertThatCode(() -> authenticateUser(environment)).doesNotThrowAnyException();

Наконец, мы закроем контекст администратора, чтобы освободить ресурсы:

adminContext.close();

7. Заключение

В этой статье мы обсудили, как использовать JNDI для аутентификации пользователя с помощью LDAP с использованием DN и пароля пользователя.

Кроме того, мы рассмотрели, как найти DN, если у нас его нет.

Как обычно, полный исходный код примеров можно найти на GitHub .