6.5. 简单只读查找示例


为了说明实施用户存储 SPI 的基础知识,我们来浏览一个简单的示例。在本章中,您将看到一个简单 UserStorageProvider 的实现,它会在一个简单的属性文件中查找用户。属性文件包含用户名和密码定义,并硬编码到 classpath 上的特定位置。该提供程序将能够按 ID 和用户名查找用户,并能够验证密码。源自此提供程序的用户将只读。

6.5.1. 供应商类

首先,我们将进入的事情是 UserStorageProvider 类。

public class PropertyFileUserStorageProvider implements
        UserStorageProvider,
        UserLookupProvider,
        CredentialInputValidator,
        CredentialInputUpdater
{
...
}

我们的供应商类 PropertyFileUserStorageProvider 实现了许多接口。它实现 UserStorageProvider,因为这是 SPI 的基本要求。它实现了 UserLookupProvider 接口,因为我们希望能够使用此提供程序存储的用户登录。它实现了 CredentialInputValidator 接口,因为我们希望能够使用登录屏幕验证输入的密码。我们的属性文件是只读的。我们实施 CredentialInputUpdater,因为当用户尝试更新其密码时,我们希望发出错误条件。

    protected KeycloakSession session;
    protected Properties properties;
    protected ComponentModel model;
    // map of loaded users in this transaction
    protected Map<String, UserModel> loadedUsers = new HashMap<>();

    public PropertyFileUserStorageProvider(KeycloakSession session, ComponentModel model, Properties properties) {
        this.session = session;
        this.model = model;
        this.properties = properties;
    }

此提供程序类的构造器将存储对 KeycloakSessionComponentModel 和 属性文件的引用。我们稍后将用到所有这些产品。另请注意,有加载的用户映射。每当我们发现某个用户,我们将将其存储在此地图中,以便我们避免在同一事务中再次重新创建。这是遵循许多提供商需要执行此操作的良好做法(即,与 JPA 集成的任何提供商)。请记住,每个事务都会创建提供程序类实例,并在事务完成后关闭。

6.5.1.1. UserLookupProvider 实现

    @Override
    public UserModel getUserByUsername(RealmModel realm, String username) {
        UserModel adapter = loadedUsers.get(username);
        if (adapter == null) {
            String password = properties.getProperty(username);
            if (password != null) {
                adapter = createAdapter(realm, username);
                loadedUsers.put(username, adapter);
            }
        }
        return adapter;
    }

    protected UserModel createAdapter(RealmModel realm, String username) {
        return new AbstractUserAdapter(session, realm, model) {
            @Override
            public String getUsername() {
                return username;
            }
        };
    }

    @Override
    public UserModel getUserById(RealmModel realm, String id) {
        StorageId storageId = new StorageId(id);
        String username = storageId.getExternalId();
        return getUserByUsername(realm, username);
    }

    @Override
    public UserModel getUserByEmail(RealmModel realm, String email) {
        return null;
    }

当用户登录时,Red Hat build of Keycloak 登录页会调用 getUserByUsername () 方法。在我们的实施中,我们首先检查 loadedUsers 映射,以查看用户是否已在此事务中载入了用户。如果没有加载,我们将查看用户名的属性文件。如果存在,我们创建一个 UserModel 的实现,将其存储在 loadUsers 中,以备将来参考,并返回此实例。

createAdapter () 方法使用帮助程序类 org.keycloak.storage.adapter.AbstractUserAdapter。这为 UserModel 提供了基础实施。它使用用户的用户名作为外部 ID,根据所需的存储 id 格式自动生成用户 id。

"f:" + component id + ":" + username

每个 get 方法 AbstractUserAdapter 都会返回 null 或空集合。但是,返回角色和组映射的方法将返回为每个用户配置域的默认角色和组。每个组 AbstractUserAdapter 都会抛出 org.keycloak.storage.ReadOnlyException。因此,如果您试图在管理控制台中修改用户,则会出现错误。

getUserById () 方法使用 org.keycloak.storage.StorageId helper 类解析 id 参数。调用 StorageId.getExternalId () 方法,以获取嵌入在 id 参数中的用户名。然后,方法将委托为 getUserByUsername ()

电子邮件不会被存储,因此 getUserByEmail () 方法返回 null。

6.5.1.2. CredentialInputValidator 实现

接下来,让我们来看看 CredentialInputValidator 的方法实施。

    @Override
    public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
        String password = properties.getProperty(user.getUsername());
        return credentialType.equals(PasswordCredentialModel.TYPE) && password != null;
    }

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

    @Override
    public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
        if (!supportsCredentialType(input.getType())) return false;

        String password = properties.getProperty(user.getUsername());
        if (password == null) return false;
        return password.equals(input.getChallengeResponse());
    }

运行时调用 isConfiguredFor () 方法,以确定是否为用户配置了特定的凭证类型。这个方法检查是否为用户设置了密码。

supportsCredentialType () 方法返回是否支持特定凭证类型的验证。我们将检查该凭证类型是否为 密码

isValid () 方法负责验证密码。CredentialInput 参数实际上是所有凭证类型的一个抽象接口。我们确保支持凭证类型,并且也是 UserCredentialModel 的实例。当用户通过登录页面登录时,密码输入的纯文本将放入 UserCredentialModel 的实例。isValid () 方法针对存储在属性文件中的纯文本密码检查这个值。返回值 true 表示密码有效。

6.5.1.3. CredentialInputUpdater 实现

如前所述,我们实现 CredentialInputUpdater 接口的唯一原因是,禁止修改用户密码。我们必须这样做的原因是,否则运行时允许在红帽构建的 Keycloak 本地存储中覆盖密码。本章稍后将对此进行更多讨论。

    @Override
    public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
        if (input.getType().equals(PasswordCredentialModel.TYPE)) throw new ReadOnlyException("user is read only for this update");

        return false;
    }

    @Override
    public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {

    }

    @Override
    public Stream<String> getDisableableCredentialTypesStream(RealmModel realm, UserModel user) {
        return Stream.empty();
    }

updateCredential () 方法只检查凭证类型是否为 password。如果是,则会抛出 ReadOnlyException

6.5.2. 供应商工厂实施

现在,供应商类已完成,我们现在将注意供应商工厂类。

public class PropertyFileUserStorageProviderFactory
                 implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {

    public static final String PROVIDER_NAME = "readonly-property-file";

    @Override
    public String getId() {
        return PROVIDER_NAME;
    }

首先要注意的是,在实施 UserStorageProviderFactory 类时,您必须传递 concrete provider 类实现作为模板参数。此处我们指定之前定义的供应商类: PropertyFileUserStorageProvider

警告

如果没有指定 template 参数,您的供应商将无法正常工作。运行时执行类内省,以确定提供程序实施 的能力接口

getId () 方法标识运行时中的工厂,当您要为域启用用户存储供应商时,也会是 admin 控制台中显示的字符串。

6.5.2.1. 初始化

    private static final Logger logger = Logger.getLogger(PropertyFileUserStorageProviderFactory.class);
    protected Properties properties = new Properties();

    @Override
    public void init(Config.Scope config) {
        InputStream is = getClass().getClassLoader().getResourceAsStream("/users.properties");

        if (is == null) {
            logger.warn("Could not find users.properties in classpath");
        } else {
            try {
                properties.load(is);
            } catch (IOException ex) {
                logger.error("Failed to load users.properties file", ex);
            }
        }
    }

    @Override
    public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
        return new PropertyFileUserStorageProvider(session, model, properties);
    }

UserStorageProviderFactory 接口具有可实现的可选 init () 方法。当红帽构建的 Keycloak 引导时,只会为每个供应商工厂创建一个实例。另外,引导时都会调用 init () 方法。还有一个 postInit () 方法,也可以实施。在调用每个工厂的 init () 方法后,会调用其 postInit () 方法。

在我们的 init () 方法实现中,我们从 classpath 中找到包含我们用户声明的属性文件。然后,使用存储了用户名和密码组合来加载 properties 字段。

Config.Scope 参数是通过服务器配置的工厂配置。

例如,使用以下参数运行服务器:

kc.[sh|bat] start --spi-storage-readonly-property-file-path=/other-users.properties

我们可以指定用户属性文件的 classpath,而不是对其进行硬编码。然后,您可以在 PropertyFileUserStorageProviderFactory.init () 中检索配置:

public void init(Config.Scope config) {
    String path = config.get("path");
    InputStream is = getClass().getClassLoader().getResourceAsStream(path);

    ...
}

6.5.2.2. 创建方法

创建供应商工厂的最后一步是 create () 方法。

    @Override
    public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
        return new PropertyFileUserStorageProvider(session, model, properties);
    }

我们只需分配 PropertyFileUserStorageProvider 类。此创建方法将为每个事务调用一次。

6.5.3. 打包和部署

用于供应商实现的类文件应放在 jar 中。您还必须在 META-INF/services/org.keycloak.storage.UserProviderFactory 文件中声明供应商工厂类。

org.keycloak.examples.federation.properties.FilePropertiesStorageFactory

要部署此 jar,将其复制到 provider/ 目录中,然后运行 bin/kc.[sh|bat] build

6.5.4. 在管理门户中启用提供程序

您可以在管理控制台的 User Federation 页面中为每个域启用用户存储供应商。

用户 Federation

empty user federation page

流程

  1. 从列表中选择刚才创建的提供程序: readonly-property-file

    这时将显示我们供应商的配置页面。

  2. 单击 Save,因为我们无需配置。

    配置的供应商

    storage provider created

  3. 返回到主 User Federation 页面

    现在,您会看到列出的供应商。

    用户 Federation

    user federation page

现在,您将能够使用 users.properties 文件中声明的用户登录。此用户只能在登录后查看帐户页面。

Red Hat logoGithubredditYoutubeTwitter

学习

尝试、购买和销售

社区

关于红帽文档

通过我们的产品和服务,以及可以信赖的内容,帮助红帽用户创新并实现他们的目标。 了解我们当前的更新.

让开源更具包容性

红帽致力于替换我们的代码、文档和 Web 属性中存在问题的语言。欲了解更多详情,请参阅红帽博客.

關於紅帽

我们提供强化的解决方案,使企业能够更轻松地跨平台和环境(从核心数据中心到网络边缘)工作。

Theme

© 2026 Red Hat
返回顶部