Рубрики
Без рубрики

Использование пользовательских поставщиков пользователей с Keycloak

Автор оригинала: Philippe Sevestre.

1. введение

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

2. Обзор пользовательских поставщиков с Keycloak

Из коробки Keycloak предоставляет ряд стандартных интеграций на основе протоколов, таких как SAML, OpenID Connect и OAuth2 . Хотя эта встроенная функциональность довольно мощная, иногда ее недостаточно . Общим требованием, особенно когда речь идет о устаревших системах, является интеграция пользователей из этих систем в Keycloak. Чтобы учесть этот и аналогичные сценарии интеграции, Keycloak поддерживает концепцию пользовательских поставщиков.

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

2.1. Развертывание и обнаружение пользовательских поставщиков

В своей простейшей форме пользовательский поставщик-это просто стандартный файл jar, содержащий одну или несколько реализаций службы. При запуске Keycloak просканирует свой путь к классу и выберет всех доступных поставщиков, используя стандартный java.util.Механизм загрузки служб . Это означает, что все, что нам нужно сделать, это создать файл, названный в честь конкретного интерфейса службы, который мы хотим предоставить в папке META-INF/services нашего jar, и поместить в него полное имя нашей реализации.

Но какие услуги мы можем добавить в Keycloak? Если мы перейдем на страницу информация о сервере , доступную в консоли управления Keycloak, мы увидим их довольно много:

На этом рисунке левый столбец соответствует заданному интерфейсу поставщика услуг (сокращенно SPI), а правый показывает доступных поставщиков для этого конкретного SPI.

2.2. Доступные SPI

В основной документации Keycloak перечислены следующие SPI:

  • org.keycloak.аутентификация.AuthenticatorFactory : Определяет действия и потоки взаимодействия, необходимые для аутентификации пользователя или клиентского приложения
  • org.keycloak.authentication.токен действия.Фабрика обработчиков токенов действий : Позволяет нам создавать пользовательские действия, которые Keycloak будет выполнять при достижении конечной точки /auth/realms/master/login-actions/action-token|/. Например, этот механизм стоит за стандартным потоком сброса пароля. Ссылка, включенная в электронное письмо, включает в себя такой токен действия org.keycloak.события.Прослушиватель событий ProviderFactory
  • : Создает поставщика, который прослушивает события Keycloak. Страница Тип события Javadoc содержит список доступных событий, которые может обрабатывать поставщик. Типичным использованием этого SPI было бы создание базы данных аудита org.keycloak.адаптеры.образец.Поставщик сопоставлений ролей
  • : Сопоставляет роли SAML, полученные от внешнего поставщика удостоверений, с ролями Keycloak. Это очень гибкое отображение, позволяющее нам переименовывать, удалять и/или добавлять роли в контексте данной области org.keycloak.storage.Фабрика поставщиков пользовательских хранилищ
  • : Позволяет Keycloak получать доступ к пользовательским хранилищам пользователей org.keycloak.vault.Vault ProviderFactory
  • : Позволяет нам использовать пользовательское хранилище для хранения секретов конкретной области. Они могут включать в себя такую информацию, как ключи шифрования, учетные данные базы данных и т. Д.

Теперь этот список ни в коем случае не охватывает всех доступных шпионов: они просто наиболее хорошо документированы и, на практике, скорее всего, потребуют настройки.

3. Реализация Пользовательского Поставщика

Как мы уже упоминали во введении к этой статье, наш пример поставщика позволит нам использовать Keycloak с пользовательским репозиторием только для чтения. Например, в нашем случае этот пользовательский репозиторий представляет собой обычную таблицу SQL с несколькими атрибутами:

create table if not exists users(
    username varchar(64) not null primary key,
    password varchar(64) not null,
    email varchar(128),
    firstName varchar(128) not null,
    lastName varchar(128) not null,
    birthDate DATE not null
);

Чтобы поддерживать это пользовательское хранилище пользователей, мы должны реализовать фабрику Поставщика хранилища пользователей SPIN и развернуть ее в существующем экземпляре Keycloak.

Ключевым моментом здесь является часть, доступная только для чтения. Под этим мы подразумеваем, что пользователи смогут использовать свои учетные данные для входа в Keycloak, но не изменять какую-либо информацию в пользовательском хранилище, включая свои пароли. Это, однако, не является ограничением блокировки клавиш, так как на самом деле поддерживает двунаправленные обновления. Встроенный поставщик LDAP является хорошим примером поставщика, поддерживающего эту функциональность.

3.1. Настройка проекта

Наш проект пользовательского поставщика-это обычный проект Maven, который создает файл jar. Чтобы избежать трудоемкого цикла компиляции-развертывания-перезапуска нашего поставщика в обычный экземпляр Keycloak, мы используем хороший трюк: встроим Keycloak в наш проект в качестве зависимости от времени тестирования.

Мы уже рассмотрели как встроить Keycloak в приложение Spring Boot , поэтому мы не будем вдаваться в подробности о том, как это делается здесь . Приняв этот метод, мы получим более быстрое время запуска и возможности горячей перезагрузки, обеспечивая более плавный опыт разработчика. Здесь мы повторно используем пример приложения Spring Boot для запуска тестов непосредственно из нашего пользовательского поставщика, поэтому мы добавим его в качестве тестовой зависимости:


    org.keycloak
    keycloak-core
    12.0.2



    org.keycloak
    keycloak-server-spi
    12.0.2



    com.baeldung
    oauth-authorization-server
    0.1.0-SNAPSHOT
    test

Мы используем последнюю версию 11-й серии для зависимостей keycloak-core и keycloak-server-spi Keycloak.

Однако зависимость oauth-authorization-server должна быть построена локально из репозитория |/Buildings Spring Security OAuth/|.

3.2. Внедрение Фабрики Поставщиков пользовательских хранилищ

Давайте начнем с нашего поставщика, создав реализацию User Storage Provider Factory и сделав ее доступной для обнаружения с помощью Keycloak.

Этот интерфейс содержит одиннадцать методов, но нам нужно реализовать только два из них:

  • getId() : Возвращает уникальный идентификатор для этого поставщика, который Keycloak покажет на своей странице администрирования.
  • create() : Возвращает фактическую реализацию поставщика.

Keycloak вызывает метод create() для каждой транзакции, передавая KeycloakSession и ComponentModel в качестве аргументов . Здесь транзакция означает любое действие, требующее доступа к хранилищу пользователей. Ярким примером является поток входа в систему: в какой-то момент Keycloak вызовет каждое настроенное хранилище пользователей для данной области для проверки учетных данных. Поэтому на данном этапе мы должны избегать каких-либо дорогостоящих действий инициализации, так как метод create() вызывается постоянно.

Тем не менее, реализация довольно тривиальна:

public class CustomUserStorageProviderFactory
  implements UserStorageProviderFactory {
    @Override
    public String getId() {
        return "custom-user-provider";
    }

    @Override
    public CustomUserStorageProvider create(KeycloakSession ksession, ComponentModel model) {
        return new CustomUserStorageProvider(ksession,model);
    }
}

Мы выбрали “custom-user-provider” для вашего идентификатора поставщика, и наша реализация create() просто возвращает новый экземпляр нашей реализации UserStorageProvider|/. Теперь мы не должны забывать создавать файл определения службы и добавлять его в наш проект. Этот файл должен быть назван org.keycloak.storage.Фабрика поставщиков пользовательских хранилищ и помещена в папку META-INF/services нашей окончательной банки.

Поскольку мы используем стандартный проект Maven, это означает, что мы добавим его в папку src/main/resources/META-INF/services :

Содержимое этого файла-это просто полное имя реализации SPI:

# SPI class implementation
com.baeldung.auth.provider.user.CustomUserStorageProviderFactory

3.3. Реализация Поставщика Пользовательских хранилищ

На первый взгляд реализация User Storage Provider выглядит не так, как мы ожидали. Он содержит всего несколько методов обратного вызова, ни один из которых не относится к реальным пользователям. Причина этого заключается в том, что Keycloak ожидает, что наш поставщик также внедрит другие интерфейсы смешивания, которые поддерживают конкретные аспекты управления пользователями.

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

Наконец, общим требованием является возможность отображения существующих пользователей в нашем пользовательском магазине в интерфейсе администратора Keycloak. Это требует, чтобы мы реализовали еще один интерфейс: Поставщик пользовательских запросов . Этот добавляет некоторые методы запросов и действует как DAO для нашего магазина.

Итак, учитывая эти требования, вот как должна выглядеть наша реализация:

public class CustomUserStorageProvider implements UserStorageProvider, 
  UserLookupProvider,
  CredentialInputValidator, 
  UserQueryProvider {
  
    // ... private members omitted
    
    public CustomUserStorageProvider(KeycloakSession ksession, ComponentModel model) {
      this.ksession = ksession;
      this.model = model;
    }

    // ... implementation methods for each supported capability
}

Обратите внимание, что мы сохраняем значения, переданные нашему конструктору. Позже мы увидим, как они играют важную роль в нашей реализации.

3.4. Реализация поставщика поиска пользователей

Keycloak использует методы в этом интерфейсе для восстановления Модели пользователя экземпляра с учетом его id , имени пользователя или электронной почты. Идентификатор, в данном случае, является уникальным идентификатором для этого пользователя, отформатированным следующим образом: ‘f:’ unique_id ‘:’ external_id

  • “f:” – это просто фиксированный префикс, указывающий, что это федеративный пользователь
  • unique_id – это идентификатор ключа для пользователя
  • external_id – это идентификатор пользователя, используемый данным хранилищем пользователей. В нашем случае это будет значение столбца username

Давайте продолжим и реализуем методы этого интерфейса, начиная с getUserByUsername() :

@Override
public UserModel getUserByUsername(String username, RealmModel realm) {
    try ( Connection c = DbUtil.getConnection(this.model)) {
        PreparedStatement st = c.prepareStatement(
          "select " +
          "  username, firstName, lastName, email, birthDate " + 
          "from users " + 
          "where username = ?");
        st.setString(1, username);
        st.execute();
        ResultSet rs = st.getResultSet();
        if ( rs.next()) {
            return mapUser(realm,rs);
        }
        else {
            return null;
        }
    }
    catch(SQLException ex) {
        throw new RuntimeException("Database error:" + ex.getMessage(),ex);
    }
}

Как и ожидалось, это простой запрос к базе данных с использованием предоставленного имени пользователя для поиска его информации. Есть два интересных момента, которые требуют некоторого объяснения: DbUtil.getConnection() и map User() .

DbUtil – это вспомогательный класс, который каким-то образом возвращает JDBC Соединение из информации, содержащейся в ComponentModel , которую мы получили в конструкторе. Мы рассмотрим его детали позже.

Что касается mapUser() , то его задача заключается в сопоставлении записей базы данных, содержащих пользовательские данные, с экземпляром Модели пользователя . A Модель пользователя представляет сущность пользователя, как видно из Keycloak, и имеет методы для чтения его атрибутов. Наша реализация этого интерфейса, доступная здесь, расширяет класс AbstractUserAdapter , предоставляемый Keycloak. Мы также добавили Builder внутренний класс в нашу реализацию, поэтому map User() может легко создавать Пользовательские модели экземпляры:

private UserModel mapUser(RealmModel realm, ResultSet rs) throws SQLException {
    CustomUser user = new CustomUser.Builder(ksession, realm, model, rs.getString("username"))
      .email(rs.getString("email"))
      .firstName(rs.getString("firstName"))
      .lastName(rs.getString("lastName"))
      .birthDate(rs.getDate("birthDate"))
      .build();
    return user;
}

Аналогичным образом, другие методы в основном следуют той же схеме, описанной выше, поэтому мы не будем подробно их рассматривать. Пожалуйста, обратитесь к коду поставщика и проверьте все методы get User By XXX и search For User .

3.5. Получение соединения

Теперь давайте взглянем на метод DbUtil.getConnection() :

public class DbUtil {

    public static Connection getConnection(ComponentModel config) throws SQLException{
        String driverClass = config.get(CONFIG_KEY_JDBC_DRIVER);
        try {
            Class.forName(driverClass);
        }
        catch(ClassNotFoundException nfe) {
           // ... error handling omitted
        }
        
        return DriverManager.getConnection(
          config.get(CONFIG_KEY_JDBC_URL),
          config.get(CONFIG_KEY_DB_USERNAME),
          config.get(CONFIG_KEY_DB_PASSWORD));
    }
}

Мы видим, что ComponentModel – это место, где находятся все необходимые параметры для создания. Но как Keycloak узнает, какие параметры требуются нашему пользовательскому поставщику? Чтобы ответить на этот вопрос, нам нужно вернуться к Custom User Storage Provider Factory.

3.6. Метаданные конфигурации

Базовый контракт для Custom User Storage Provider Factory , User Storage Provider Factory содержит методы , которые позволяют Keycloak запрашивать метаданные свойств конфигурации и, что также важно, проверять назначенные значения . В нашем случае мы определим несколько параметров конфигурации, необходимых для установления соединения JDBC. Поскольку эти метаданные статичны, мы создадим их в конструкторе, и getConfigProperties() просто вернет их.

public class CustomUserStorageProviderFactory
  implements UserStorageProviderFactory {
    protected final List configMetadata;
    
    public CustomUserStorageProviderFactory() {
        configMetadata = ProviderConfigurationBuilder.create()
          .property()
            .name(CONFIG_KEY_JDBC_DRIVER)
            .label("JDBC Driver Class")
            .type(ProviderConfigProperty.STRING_TYPE)
            .defaultValue("org.h2.Driver")
            .helpText("Fully qualified class name of the JDBC driver")
            .add()
          // ... repeat this for every property (omitted)
          .build();
    }
    // ... other methods omitted
    
    @Override
    public List getConfigProperties() {
        return configMetadata;
    }

    @Override
    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config)
      throws ComponentValidationException {
       try (Connection c = DbUtil.getConnection(config)) {
           c.createStatement().execute(config.get(CONFIG_KEY_VALIDATION_QUERY));
       }
       catch(Exception ex) {
           throw new ComponentValidationException("Unable to validate database connection",ex);
       }
    }
}

В validateConfiguration () мы получим все необходимое для проверки параметров , переданных при добавлении в область . В нашем случае мы используем эту информацию для установления соединения с базой данных и выполнения запроса проверки. Если что-то пойдет не так, мы просто выбрасываем исключение Component ValidationException , сигнализируя Keycloak, что параметры недопустимы.

Кроме того, хотя это и не показано здесь, мы также можем использовать метод oncreate() для присоединения логики, которая будет выполняться каждый раз, когда администратор добавляет нашего поставщика в Область . Это позволяет нам выполнять логику одноразовой инициализации для подготовки нашего хранилища к использованию, которая может потребоваться для определенных сценариев. Например, мы могли бы использовать этот метод для изменения нашей базы данных

3.7. Реализация Валидатора Ввода Учетных данных

Этот интерфейс содержит методы, которые проверяют учетные данные пользователя . Поскольку Keycloak поддерживает различные типы учетных данных (пароль, токены OTP, сертификаты X. 509 и т.д.), наш поставщик должен сообщить, поддерживает ли он данный тип в supportsCredentialType () | и настроен для него в контексте данной области в isConfiguredFor() .

В нашем случае у нас просто есть поддержка паролей, и, поскольку они не требуют дополнительной настройки, мы можем делегировать более поздний метод первому:

@Override
public boolean supportsCredentialType(String credentialType) {
    return PasswordCredentialModel.TYPE.endsWith(credentialType);
}

@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
    return supportsCredentialType(credentialType);
}

Фактическая проверка пароля происходит в методе isValid() :

@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) {
    if(!this.supportsCredentialType(credentialInput.getType())) {
        return false;
    }
    StorageId sid = new StorageId(user.getId());
    String username = sid.getExternalId();
    
    try (Connection c = DbUtil.getConnection(this.model)) {
        PreparedStatement st = c.prepareStatement("select password from users where username = ?");
        st.setString(1, username);
        st.execute();
        ResultSet rs = st.getResultSet();
        if ( rs.next()) {
            String pwd = rs.getString(1);
            return pwd.equals(credentialInput.getChallengeResponse());
        }
        else {
            return false;
        }
    }
    catch(SQLException ex) {
        throw new RuntimeException("Database error:" + ex.getMessage(),ex);
    }
}

Здесь есть несколько моментов, которые стоит обсудить. Во-первых, обратите внимание, как мы извлекаем внешний идентификатор из Модели пользователя, используя Идентификатор хранилища объект, инициализированный из идентификатора Keycloak. Мы могли бы использовать тот факт, что этот идентификатор имеет хорошо известный формат, и извлечь оттуда имя пользователя, но лучше перестраховаться здесь и позволить этим знаниям инкапсулироваться в классе, предоставляемом Keycloak.

Далее следует фактическая проверка пароля. Для нашей упрощенной и, конечно, очень небезопасной базы данных проверка пароля тривиальна: просто сравните значение базы данных с предоставленным пользователем значением, доступным через getChallengeResponse() , и мы закончим. Конечно, для реального поставщика потребуется еще несколько шагов, таких как создание хэш-пароля и значения соли из базы данных и сравнение хэшей.

Наконец, пользовательские хранилища обычно имеют некоторый жизненный цикл, связанный с паролями: максимальный возраст, заблокированный и/или неактивный статус и так далее. Независимо от этого, при реализации поставщика метод isValid() является местом для добавления этой логики.

3.8. Реализация UserQueryProvider

Интерфейс User Query Provider capability сообщает Keycloak, что наш поставщик может искать пользователей в своем магазине. Это очень удобно, так как, поддерживая эту возможность, мы сможем видеть пользователей в консоли администратора.

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

Поскольку реализация этих методов довольно схожа, давайте рассмотрим только один из них, searchForUser() :

@Override
public List searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
    try (Connection c = DbUtil.getConnection(this.model)) {
        PreparedStatement st = c.prepareStatement(
          "select " + 
          "  username, firstName, lastName, email, birthDate " +
          "from users " + 
          "where username like ? + 
          "order by username limit ? offset ?");
        st.setString(1, search);
        st.setInt(2, maxResults);
        st.setInt(3, firstResult);
        st.execute();
        ResultSet rs = st.getResultSet();
        List users = new ArrayList<>();
        while(rs.next()) {
            users.add(mapUser(realm,rs));
        }
        return users;
    }
    catch(SQLException ex) {
        throw new RuntimeException("Database error:" + ex.getMessage(),ex);
    }
}

Как мы видим, здесь нет ничего особенного: просто обычный код JDBC. Примечание по реализации, заслуживающее упоминания: Поставщик пользовательских запросов методы обычно бывают в подкачанной и не подкачанной версиях. Поскольку пользовательское хранилище потенциально может содержать большое количество записей, версии без подкачки должны просто делегироваться подкачанным версиям, используя разумное значение по умолчанию. Еще лучше, мы можем добавить параметр конфигурации, который определяет, что такое “разумное значение по умолчанию”.

4. Тестирование

Теперь, когда мы внедрили нашего провайдера, пришло время протестировать его локально, используя встроенный экземпляр Keycloak. Код проекта содержит живой тестовый класс, который мы использовали для начальной загрузки ключа и пользовательской базы данных пользователей, а затем просто распечатали URL-адреса доступа на консоли, прежде чем спать в течение одного часа.

Используя эту настройку, мы можем убедиться, что наш пользовательский поставщик работает должным образом, просто открыв напечатанный URL-адрес в браузере:

Для доступа к консоли администрирования мы будем использовать учетные данные администратора, которые мы можем получить, просмотрев файл application-test.yml . После входа в систему перейдите на страницу “Информация о сервере”.:

На вкладке “Поставщики” мы видим, что наш пользовательский поставщик отображается вместе с другими встроенными поставщиками хранилища:

Мы также можем проверить, что область Баельдунга уже использует этого поставщика. Для этого мы можем выбрать его в раскрывающемся меню в левом верхнем углу, а затем перейти на страницу Федерация пользователей :

Далее, давайте проверим фактический вход в эту область. Мы будем использовать страницу управления учетными записями realms, где пользователь может управлять своими данными. Наш живой тест напечатает этот URL-адрес перед переходом в спящий режим, поэтому мы можем просто скопировать его с консоли и вставить в адресную строку браузера.

Тестовые данные содержат трех пользователей: user1, user2 и user3. Пароль для всех них один и тот же: “измените его”. После успешного входа в систему мы увидим страницу управления учетной записью, на которой отображаются данные импортированного пользователя:

Однако, если мы попытаемся изменить какие-либо данные, мы получим ошибку. Это ожидаемо, так как наш провайдер доступен только для чтения, поэтому Keycloak не позволяет изменять его. На данный момент мы оставим все как есть, поскольку поддержка двунаправленной синхронизации выходит за рамки этой статьи.

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

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