Using Custom User Providers with Keycloak – 在Keycloak中使用自定义用户提供者

最后修改: 2021年 1月 24日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

In this tutorial, we’ll show how to add a custom provider to Keycloak, a popular open-source identity management solution, so we can use it with existing and/or non-standard user stores.

在本教程中,我们将展示如何向Keycloak(一种流行的开源身份管理解决方案)添加自定义提供者,以便我们能够将其用于现有和/或非标准的用户存储。

2. Overview of Custom Providers with Keycloak

2.使用Keycloak的定制供应商概述

Out-of-the-box, Keycloak provides a range of standard-based integrations based on protocols like SAML, OpenID Connect, and OAuth2. While this built-in functionality is quite powerful, sometimes it’s not enough. A common requirement, especially when legacy systems are involved, is to integrate users from those systems into Keycloak. To accommodate for this and similar integration scenarios, Keycloak supports the concept of custom providers.

开箱即用,Keycloak提供了一系列基于标准的集成,这些协议包括SAML、OpenID Connect和OAuth2虽然这种内置功能相当强大,但有时却不够用。一个常见的要求,特别是当涉及到遗留系统时,是将这些系统的用户整合到Keycloak。为了适应这种和类似的整合情况,Keycloak支持自定义提供者的概念。

Custom providers play a key role in Keycloak’s architecture. For every major functionality, like the login flow, authentication, authorization, there’s a corresponding Service Provider Interface. This approach allows us to plug custom implementations for any of those services, which Keycloak will then use as it were one of its own.

自定义提供者在Keycloak的架构中起着关键作用。对于每个主要的功能,如登录流程,认证,授权,都有一个相应的服务提供者接口。这种方法允许我们为任何这些服务插入定制的实现,然后Keycloak会像使用自己的服务一样使用。

2.1. Custom Provider Deployment and Discovery

2.1.自定义提供者的部署和发现

In its simplest form, a custom provider is just a standard jar file containing one or more service implementations. At startup, Keycloak will scan its classpath and pick all available providers using the standard java.util.ServiceLoader mechanism. This means all we have to do is to create a file named after the specific service interface we want to provide in the META-INF/services folder of our jar and put the fully qualified name of our implementation in it.

在其最简单的形式中,自定义提供者只是一个包含一个或多个服务实现的标准jar文件。在启动时,Keycloak将扫描其classpath并使用标准的java.util.ServiceLoader机制挑选所有可用的提供者。这意味着我们所要做的就是在我们的jar的META-INF/services文件夹中创建一个以我们想要提供的特定服务接口命名的文件,并将我们的实现的完全合格的名称放入其中。

But, what kind of services can we add to Keycloak? If we go to the server info page, available at Keycloak’s management console, we’ll see quite a lot of them:

但是,我们可以向Keycloak添加什么样的服务?如果我们进入Keycloak的管理控制台的server info页面,我们会看到相当多的服务。

In this picture, the left column corresponds to a given Service Provider Interface (SPI, for short), and the right one shows the available providers for that particular SPI.

在这幅图中,左边一栏对应于一个给定的服务提供商接口(简称SPI),右边一栏显示该特定SPI的可用提供商。

2.2. Available SPIs

2.2.可用的SPI

Keycloak’s main documentation lists the following SPIs:

Keycloak的主要文件列出了以下SPI。

  • org.keycloak.authentication.AuthenticatorFactory: Defines actions and interaction flows required to authenticate a user or client application
  • org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory: Allows us to create custom actions that Keycloak will perform upon reaching the /auth/realms/master/login-actions/action-token endpoint. As an example, this mechanism is behind the standard password reset flow. The link included in the e-mail includes such an action token
  • org.keycloak.events.EventListenerProviderFactory: Creates a provider that listens for Keycloak events. The EventType Javadoc page contains a list of the available events custom a provider can handle. A typical use for using this SPI would be creating an audit database
  • org.keycloak.adapters.saml.RoleMappingsProvider: Maps SAML roles received from an external identity provider into Keycloak’s ones. This mapping very flexible, allowing us to rename, remove, and/or add roles in the context of a given Realm
  • org.keycloak.storage.UserStorageProviderFactory: Allows Keycloak to access custom user stores
  • org.keycloak.vault.VaultProviderFactory: Allows us to use a custom vault to store Realm-specific secrets. Those can include information like encryption keys, database credentials, etc.

Now, this list by no means covers all the available SPIs: they’re just the most well documented and, in practice, most likely to require customization.

现在,这个列表决不是涵盖了所有可用的SPI:它们只是记录得最清楚的,而且在实践中,最可能需要定制。

3. Custom Provider Implementation

3.自定义提供者的实施

As we’ve mentioned in this article’s introduction, our provider example will allow us to use Keycloak with a read-only custom user repository. For instance, in our case, this user repository is just a regular SQL table with a few attributes:

正如我们在本文的介绍中提到的,我们的提供者的例子将允许我们使用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
);

To support this custom user store, we have to implement the UserStorageProviderFactory SPI and deploy it into an existing Keycloak instance.

为了支持这个自定义的用户存储,我们必须实现UserStorageProviderFactory SPI并将其部署到现有的Keycloak实例中。

A key point here is the read-only part. By that, we mean that users will be able to use their credentials to log in to Keycloak, but not to change any information in the custom store, including their passwords. This, however, is not a Keycloak limitation, as it actually supports bi-directional updates. The built-in LDAP provider is a good example of a provider that supports this functionality.

这里的一个关键点是只读部分。我们的意思是,用户将能够使用他们的证书来登录Keycloak,但不能改变自定义商店中的任何信息,包括他们的密码。然而,这不是Keycloak的限制,因为它实际上支持双向更新。内置的LDAP提供者是一个支持这种功能的好例子。

3.1. Project Setup

3.1.项目设置

Our custom provider project is just a regular Maven project that creates a jar file. To avoid a time-consuming compile-deploy-restart cycle of our provider into a regular Keycloak instance, we’ll use a nice trick: embed Keycloak in our project as a test-time dependency.

我们的自定义提供者项目只是一个普通的Maven项目,它创建了一个jar文件。为了避免我们的提供者进入一个普通的Keycloak实例时出现耗时的编译-部署-重启循环,我们将使用一个不错的技巧:将Keycloak嵌入我们的项目中,作为测试时的依赖。

We’ve already covered how to embed Keycloack in a SpringBoot application, so we won’t go into details on how it’s done here. By adopting this technique, we’ll get faster start times, and hot reload capabilities, providing a smoother developer experience. Here, we’ll reuse the example SpringBoot application to run our tests directly from our custom provider, so we’ll add it in as a test dependency:

我们已经介绍了如何将Keycloack嵌入到SpringBoot应用程序中,因此我们不会在这里详细介绍它是如何做到的。通过采用这种技术,我们将获得更快的启动时间和热重载功能,从而为开发人员提供更流畅的体验。在这里,我们将重新使用SpringBoot应用程序的例子,直接从我们的自定义提供者中运行我们的测试,因此我们将把它作为一个测试依赖项添加进去。

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-core</artifactId>
    <version>12.0.2</version>
</dependency>

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-server-spi</artifactId>
    <version>12.0.2</version>
</dependency>

<dependency>
    <groupId>com.baeldung</groupId>
    <artifactId>oauth-authorization-server</artifactId>
    <version>0.1.0-SNAPSHOT</version>
    <scope>test</scope>
</dependency>

We’re using the latest 11-series version for the keycloak-core and keycloak-server-spi Keycloak dependencies.

我们使用最新的11系列版本,用于keycloak-corekeycloak-server-spi Keycloak依赖。

The oauth-authorization-server dependency, however, must be built locally from Baeldung’s Spring Security OAuth repository.

然而,oauth-authorization-server依赖项必须从Baeldung的Spring Security OAuth 仓库本地构建。

3.2. UserStorageProviderFactory Implementation

3.2.UserStorageProviderFactory实现

Let’s start our provider by creating the UserStorageProviderFactory implementation and make it available for discovery by Keycloak.

让我们通过创建UserStorageProviderFactory实现来开始我们的提供者,并让它可被Keycloak发现。

This interface contains eleven methods, but we need to implement just two of them:

这个接口包含11个方法,但我们只需要实现其中的两个。

  • getId(): Returns a unique identifier for this provider that Keycloak will show on its administration page.
  • create(): Returns the actual Provider implementation.

Keycloak invokes the create() method for every transaction, passing a KeycloakSession and a ComponentModel as arguments. Here, a transaction means any action that requires access to the user store. The prime example is the login flow: at some point, Keycloak will invoke every configured user storage for a given Realm to validate a credential. Therefore, we should avoid doing any costly initialization actions at this point, as the create() method is called all the time.

Keycloak为每个事务调用create()方法,传递一个KeycloakSession和一个ComponentModel作为参数。在这里,事务意味着任何需要访问用户存储的动作。最典型的例子是登录流程:在某些时候,Keycloak将为一个给定的Realm调用每个配置的用户存储来验证一个证书。因此,我们应该避免在这个时候做任何昂贵的初始化动作,因为create()方法一直被调用。

That said, the implementation is quite trivial:

这就是说,实现起来是非常微不足道的。

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

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

We’ve chosen “custom-user-provider” for our provider id, and our create() implementation simply returns a new instance of our UserStorageProvider implementation. Now, we must not forget to create a service definition file and add it to our project. This file should be named org.keycloak.storage.UserStorageProviderFactory and placed in the META-INF/services folder of our final jar.

我们选择了“custom-user-provider”作为我们的提供者ID,而我们的create()实现只是返回一个我们的UserStorageProvider实现的新实例。现在,我们一定不要忘记创建一个服务定义文件,并将其添加到我们的项目中。这个文件应该被命名为org.keycloak.storage.UserStorageProviderFactory,并放置在我们最终jar的META-INF/services文件夹中。

Since we’re using a standard Maven project, this means we’ll add it in the src/main/resources/META-INF/services folder:

由于我们使用的是标准的Maven项目,这意味着我们要把它添加到src/main/resources/META-INF/services文件夹中。

The content of this file is just the fully qualified name of the SPI implementation:

这个文件的内容只是SPI实现的完全限定名称。

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

3.3. UserStorageProvider Implementation

3.3.UserStorageProvider实现

At first sight, the UserStorageProvider implementation doesn’t look as we’d expect. It contains just a few callback methods, none of which relates to actual users. The reason for this is that Keycloak expects our provider to also implement other mix-in interfaces that support specific user management aspects.

乍一看, UserStorageProvider实现并不像我们所期望的那样。它只包含几个回调方法,其中没有一个与实际用户有关。其原因是Keycloak期望我们的提供者也能实现其他支持特定用户管理方面的混合接口。

The full list of available interfaces is available in Keycloak’s documentation, where they’re referred to as Provider Capabilities. For a simple, read-only provider, the only interface we need to implement is UserLookupProvider. It provides just lookup capabilities, meaning that Keycloak will automatically import a user to its internal database when required. The original user’s password, however, will not be used for authentication. To do so, we also need to implement CredentialInputValidator.

可用接口的完整列表可在Keycloak的文档中找到,在那里它们被称为提供者的能力。对于一个简单的、只读的提供者,我们需要实现的唯一接口是UserLookupProvider它只提供查找功能,这意味着Keycloak将在需要时自动导入一个用户到其内部数据库。然而,原始用户的密码将不会被用于认证。要做到这一点,我们还需要实现CredentialInputValidator

Finally, a common requirement is the ability to display the existing users in our custom store in Keycloak’s admin interface. This requires that we implement yet another interface: UserQueryProvider. This one adds some query methods and acts as a DAO for our store.

最后,一个常见的要求是能够在Keycloak的管理界面中显示我们自定义商店的现有用户。这需要我们实现另一个接口。UserQueryProvider。这个接口增加了一些查询方法,并作为我们商店的DAO。

So, given those requirements, this is how our implementation should look:

因此,鉴于这些要求,我们的实施应该是这样的。

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
}

Note that we’re saving the values passed to our constructor. We’ll later see how they play an important role in our implementation.

注意,我们正在保存传递给我们构造函数的值。我们以后会看到它们在我们的实现中是如何发挥重要作用的。

3.4. UserLookupProvider Implementation

3.4.UserLookupProvider实现

Keycloak uses the methods in this interface to recover a UserModel instance given its id, username, or email. The id, in this case, is the unique identifier for this user, formatted as this: ‘f:’ unique_id ‘:’ external_id

Keycloak使用这个接口中的方法来恢复一个UserModel实例,给定其id,用户名,或电子邮件。在这种情况下,id是这个用户的唯一标识符,格式是这样的。’f:’ unique_id ‘:’ external_id‘。

  • ‘f:’ is just a fixed prefix that indicates that this is a federated user
  • unique_id is Keycloak’s id for the user
  • external_id is the user identifier used by a given user store. In our case, that would be the value of the username column

Let’s go ahead and implement this interface’s methods, starting with getUserByUsername():

让我们继续实现这个接口的方法,从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);
    }
}

As expected, this is a simple database query using the provided username to lookup its information. There are two interesting points that need some explanation: DbUtil.getConnection() and mapUser().

正如预期的那样,这是一个简单的数据库查询,使用提供的用户名来查询其信息。有两个有趣的点需要解释一下。DbUtil.getConnection()mapUser()

The DbUtil is a helper class that somehow returns a JDBC Connection from information contained in the ComponentModel that we’ve acquired in the constructor. We’ll cover its details later on.

DbUtil是一个辅助类,它以某种方式从我们在构造函数中获得的ComponentModel中的信息返回JDBCConnection。我们将在后面介绍它的细节。

As for mapUser(), its job is to map database records containing user data to a UserModel instance. A UserModel represents a user entity, as seen by Keycloak, and has methods to read its attributes. Our implementation of this interface, available here, extends the AbstractUserAdapter class provided by Keycloak. We’ve also added a Builder inner class to our implementation, so mapUser() can create UserModel instances easily:

至于mapUser(),它的工作是将包含用户数据的数据库记录映射到UserModel实例。一个UserModel代表一个用户实体,如Keycloak所见,并有方法来读取其属性。我们对这个接口的实现,可以在这里找到,它扩展了Keycloak提供的AbstractUserAdapter类。我们还为我们的实现添加了一个Builder内部类,所以mapUser() 可以轻松地创建UserModel 实例。

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;
}

Similarly, the other methods basically follow the same pattern described above, so we’ll not cover them in detail. Please refer to the provider’s code and check all the getUserByXXX and searchForUser methods.

同样,其他方法基本上也遵循上述的模式,所以我们不详细介绍。请参考提供者的代码,检查所有getUserByXXXsearchForUser方法。

3.5. Getting a Connection

3.5.获得一个连接

Now, let’s take a look at the DbUtil.getConnection() method:

现在,让我们看一下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));
    }
}

We can see that ComponentModel is where all required parameters to create are. But, how Keycloak knows which parameters our custom provider requires? To answer this question, we need to go back to CustomUserStorageProviderFactory.

我们可以看到,ComponentModel是创建所有需要的参数的地方。但是,Keycloak如何知道我们的自定义提供者需要哪些参数?为了回答这个问题,我们需要回到CustomUserStorageProviderFactory.

3.6. Configuration Metadata

3.6.配置元数据

The basic contract for CustomUserStorageProviderFactory, UserStorageProviderFactory, contains methods that allow Keycloak to query for configuration properties metadata and, also important, to validate assigned values. In our case, we’ll define a few configuration parameters required to establish a JDBC Connection. Since this metadata is static, we’ll create it in the constructor, and getConfigProperties() will simply return it.

CustomUserStorageProviderFactory的基本契约,UserStorageProviderFactory,包含了允许Keycloak查询配置属性元数据的方法,同样重要的是,验证分配的值。在我们的案例中,我们将定义一些建立JDBC连接所需的配置参数。由于这个元数据是静态的,我们将在构造函数中创建它,并且getConfigProperties() 将简单地返回它。

public class CustomUserStorageProviderFactory
  implements UserStorageProviderFactory<CustomUserStorageProvider> {
    protected final List<ProviderConfigProperty> 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<ProviderConfigProperty> 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);
       }
    }
}

In validateConfiguration(), we’ll get everything we need to validate the parameters passed on when our provided was added to a Realm. In our case, we use this information to establish a database connection and execute the validation query. If something goes wrong, we just throw a ComponentValidationException, signaling Keycloak that the parameters are invalid.

validateConfiguration()中,我们将得到我们所需要的一切,以验证我们所提供的被添加到一个Realm时传递的参数。在我们的案例中,我们使用这些信息来建立一个数据库连接并执行验证查询。如果出了问题,我们就抛出一个ComponentValidationException,告诉Keycloak参数无效。

Moreover, although not shown here, we can also use the onCreated() method to attach logic that will be executed every time an administrator adds our provider to a Realm. This allows us to execute one-time initialization-time logic to prepare our store for use, which may be necessary for certain scenarios. For instance, we could use this method to modify our database and add a column to record whether a given user already used Keycloak.

此外,虽然这里没有显示,但我们也可以使用onCreated()方法来附加逻辑,这些逻辑将在管理员每次将我们的提供者添加到 Realm 时执行。这允许我们执行一次性的初始化逻辑,以准备我们的存储空间的使用,这在某些情况下可能是必要的。例如,我们可以使用这个方法来修改我们的数据库,并添加一个列来记录一个特定的用户是否已经使用Keycloak。

3.7. CredentialInputValidator Implementation

3.7.CredentialInputValidator实现

This interface contains methods that validate user credentials. Since Keycloak supports different types of credentials (password, OTP tokens, X.509 certificates, etc.), our provider must inform if it supports a given type in supportsCredentialType() and is configured for it in the context of a given Realm in isConfiguredFor().

这个接口包含验证用户凭证的方法。由于Keycloak支持不同类型的证书(密码、OTP令牌、X.509证书等),我们的提供者必须在supportsCredentialType()中告知它是否支持给定的类型,并在isConfiguredFor()中告知它在给定的Realm背景下被配置。

In our case, we just have support to passwords and, since they do not require any extra configuration, we can delegate the later method to the former:

在我们的案例中,我们只是支持密码,由于它们不需要任何额外的配置,我们可以将后面的方法委托给前者。

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

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

The actual password validation happens in the isValid() method:

实际的密码验证发生在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);
    }
}

Here, there are a couple of points worth discussing. First, notice how we’re extracting the external id from the UserModel, using a StorageId object initialized from Keycloak’s id. We could use the fact that this id has a well-known format and extract the username from there, but it’s better to play safe here and let this knowledge encapsulated in a Keycloak-provided class.

这里,有几个要点值得讨论。首先,注意到我们如何从UserModel中提取外部id,使用一个从Keycloak的id初始化的StorageId对象。我们可以利用这个id有一个众所周知的格式的事实,并从那里提取用户名,但在这里最好还是安全一点,让这些知识封装在一个Keycloak提供的类中。

Next, there’s the actual password validation. For our simplistic and, granted, very much insecure database, password checking is trivial: just compare the database value with the user-supplied value, available through getChallengeResponse(), and we’re done. Of course, a real-world provider would require some more steps, such as generating a hash informed password and salt value from the database and compare hashes.

接下来,就是实际的密码验证了。对于我们这个简单的、当然也是非常不安全的数据库来说,密码检查是微不足道的:只需将数据库的值与用户提供的值进行比较,通过getChallengeResponse()即可,然后我们就完成了。当然,一个真实世界的提供者会需要一些更多的步骤,比如从数据库中生成一个哈希值告知密码和盐值,并比较哈希值。

Finally, user stores usually have some lifecycle associated with passwords: maximum age, blocked and/or inactive status, and so on. Regardless, when implementing a provider, the isValid() method is the place to add this logic.

最后,用户存储通常有一些与密码相关的生命周期:最大年龄、被封锁和/或不活跃状态,等等。无论怎样,当实现一个提供者时,isValid()方法是添加这种逻辑的地方。

3.8. UserQueryProvider Implementation

3.8.UserQueryProvider实现

The UserQueryProvider capability interface tells Keycloak that our provider can search users in its store. This comes in handy as, by supporting this capability, we’ll be able to see users in the admin console.

UserQueryProvider能力接口告诉Keycloak,我们的提供者可以在其商店中搜索用户。这很方便,因为通过支持这种能力,我们将能够在管理控制台看到用户。

The methods of this interface include getUsersCount(), to get the total number of users in the store, and several getXXX() and searchXXX() methods. This query interface supports looking up not only users but also groups, which we’ll not cover this time.

这个接口的方法包括getUsersCount(),以获得商店里的用户总数,以及一些getXXX()searchXXX()方法。这个查询接口不仅支持查询用户,还支持查询组,我们这次不会涉及。

As the implementation of those methods is quite similar, let’s take a look at just one of them, searchForUser():

由于这些方法的实现非常相似,让我们只看一下其中的一个,searchForUser()

@Override
public List<UserModel> 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<UserModel> users = new ArrayList<>();
        while(rs.next()) {
            users.add(mapUser(realm,rs));
        }
        return users;
    }
    catch(SQLException ex) {
        throw new RuntimeException("Database error:" + ex.getMessage(),ex);
    }
}

As we can see, there’s nothing special here: just regular JDBC code. An implementation note worth mentioning: UserQueryProvider methods usually come in paged and non-paged versions. Since a user store can potentially have a large number of records, the non-paged versions should simply delegate to the paged versions, using a sensible default. Even better, we can add a configuration parameter that defines what a “sensible default” is.

我们可以看到,这里没有什么特别之处:只是常规的JDBC代码。一个值得一提的实施说明。UserQueryProvider方法通常有分页和非分页版本。由于一个用户商店可能有大量的记录,非分页版本应该简单地委托给分页版本,使用一个合理的默认值。甚至更好的是,我们可以添加一个配置参数来定义什么是 “合理的默认值”。

4. Testing

4.测试

Now that we’ve implemented our provider, it’s time to test it locally using an embedded Keycloak instance. The project’s code contains a live test class that we’ve used to bootstrap the Keycloak and custom user database and then just print the access URLs on the console before sleeping for one hour.

现在我们已经实现了我们的提供者,是时候使用一个嵌入式的Keycloak实例在本地测试它了。这个项目的代码包含了一个实时测试类,我们用它来引导Keycloak和自定义用户数据库,然后在睡觉前在控制台打印访问URLs。

Using this setup, we can verify that our custom provider works as intended simply by opening the printed URL in a browser:

使用这种设置,我们可以通过在浏览器中打开打印的URL,来验证我们的自定义提供者是否按预期工作。

To access the administration console, we’ll use the administrator credentials, which we can get by looking at the application-test.yml file. Once logged in, let’s navigate to the “Server Info” page:

为了访问管理控制台,我们将使用管理员证书,我们可以通过查看 application-test.yml文件来获得该证书。登录后,让我们导航到 “服务器信息 “页面。

On the “Providers” tab, we can see our custom provider displayed alongside other built-in storage providers:

在 “提供商 “选项卡上,我们可以看到我们的自定义提供商与其他内置存储提供商一起显示。

We can also check that the Baeldung realm is already using this provider. For this, we can select it on the top-left drop-down menu and then navigate to the User Federation page:

我们还可以检查Baeldung境界是否已经在使用这个提供者。为此,我们可以在左上角的下拉菜单中选择它,然后导航到用户联盟页面。

Next, let’s test an actual login into this realm. We’ll use the realm’s account management page, where a user can manage its data. Our Live Test will print this URL before going into sleep, so we can just copy it from the console and paste it into the browser’s address bar.

接下来,让我们测试一下实际登录到这个领域的情况。我们将使用该领域的账户管理页面,用户可以在那里管理其数据。我们的实时测试将在进入睡眠状态前打印这个URL,所以我们可以从控制台中复制它并将其粘贴到浏览器的地址栏。

The test data contains three users: user1, user2, and user3. The password for all of them is the same: “changeit”. Upon successful login, we’ll see the account management page displaying the imported user’s data:

测试数据包含三个用户:user1,user2,和user3。他们的密码都是一样的:”changeit”。登录成功后,我们会看到账户管理页面显示导入用户的数据。

However, if we try to modify any data, we’ll get an error. This is expected, as our provider is read-only, so Keycloak doesn’t allow to modify it. For now, we’ll leave it as is since supporting bi-directional synchronization is beyond the scope of this article.

然而,如果我们试图修改任何数据,我们会得到一个错误。这是意料之中的,因为我们的提供者是只读的,所以Keycloak不允许修改它。现在,我们将保持现状,因为支持双向同步已经超出了本文的范围。

5. Conclusion

5.总结

In this article, we’ve shown how to create a custom provider for Keycloak, using a User Storage Provider as a concrete example. The full source code of the examples can be found over on GitHub.

在这篇文章中,我们已经展示了如何为Keycloak创建一个自定义的提供者,使用用户存储提供者作为一个具体的例子。这些例子的完整源代码可以在GitHub上找到over