7.4. 打包和部署


为了让红帽单点登录识别提供程序,您需要向 JAR 添加文件: META-INF/services/org.keycloak.storage.UserStorageProviderFactory。此文件必须包含 UserStorageProviderFactory 实施的完全限定类名列表:

org.keycloak.examples.federation.properties.ClasspathPropertiesStorageFactory
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
Copy to Clipboard Toggle word wrap

要部署此 jar,只需将其复制到 standalone/deployments/ 目录中。=== Simple read-only, lookup 示例

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

7.4.1. 供应商类

首先要了解的是 UserStorageProvider 类。

public class PropertyFileUserStorageProvider implements
        UserStorageProvider,
        UserLookupProvider,
        CredentialInputValidator,
        CredentialInputUpdater
{
...
}
Copy to Clipboard Toggle word wrap

我们的供应商类别 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;
    }
Copy to Clipboard Toggle word wrap

此提供程序类的构造器将存储对 KeycloakSession、CoonModel 和 属性文件的引用。稍后我们将使用所有这些产品。另请注意,有加载的用户映射。每当我们找到某个用户时,我们将将其存储在此地图中,这样我们都避免在同一交易中再次重新命名。这是为了遵守许多提供商,需要达到此目的(即,任何与 JPA 集成的供应商)的良好做法。请记住,每个事务一次创建提供程序类实例,并在事务完成后关闭。

7.4.1.1. UserLookupProvider 实现

    @Override
    public UserModel getUserByUsername(String username, RealmModel realm) {
        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(String id, RealmModel realm) {
        StorageId storageId = new StorageId(id);
        String username = storageId.getExternalId();
        return getUserByUsername(username, realm);
    }

    @Override
    public UserModel getUserByEmail(String email, RealmModel realm) {
        return null;
    }
Copy to Clipboard Toggle word wrap

当用户登录时,Red Hat Single Sign-On 登录页面会调用 getUserByUsername () 方法。在我们的实施中,我们首先检查 加载的Users 映射,以查看该用户是否已在此事务中载入。如果尚未加载,我们查看了用户名的属性文件。如果存在,我们创建了 UserModel 实施,将其存储在 loadUsers 中以供以后参考,然后返回此实例。

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

"f:" + component id + ":" + username
Copy to Clipboard Toggle word wrap

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

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

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

7.4.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());
    }
Copy to Clipboard Toggle word wrap

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

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

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

7.4.1.3. CredentialInputUpdater 实现

如前所述,本例中我们实现 CredentialInputUpdater 接口的唯一原因是,对用户密码的修改是强制修改。我们必须这样做的原因是,由于该运行时可在 Red Hat Single Sign-On 本地存储中覆盖密码。本章稍后会对此进行讨论。

    @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 Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
        return Collections.EMPTY_SET;
    }
Copy to Clipboard Toggle word wrap

updateCredential () 方法只检查凭证类型是否为密码。如果是,则引发 ReadOnlyException

7.4.2. 供应商工厂实施

现在,该提供商课程已经完成,现在我们把注意力转向了提供商的工厂级。

public class PropertyFileUserStorageProviderFactory
                 implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {

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

    @Override
    public String getId() {
        return PROVIDER_NAME;
    }
Copy to Clipboard Toggle word wrap

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

警告

如果没有指定模板参数,您的供应商将无法正常工作。运行时通过类内省来确定提供程序实施 的功能接口

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

7.4.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);
    }
Copy to Clipboard Toggle word wrap

UserStorageProviderFactory 接口具有可实施的可选 init () 方法。当 Red Hat Single Sign-On 引导时,每个提供程序工厂仅创建一个实例。另外,init () 方法会在每次工厂实例上调用。您还可以实施 postInit () 方法。调用每个工厂的 init () 方法后,会调用其 postInit () 方法。

init () 方法实施中,我们找到含有 classpath 中用户声明的属性文件。然后,我们使用在其中存储的用户名和密码载入 properties 字段。

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

例如,将以下内容添加到 standalone.xml 中:

<spi name="storage">
    <provider name="readonly-property-file" enabled="true">
        <properties>
            <property name="path" value="/other-users.properties"/>
        </properties>
    </provider>
</spi>
Copy to Clipboard Toggle word wrap

我们可以指定用户属性文件的类路径,而不是硬编码。然后,您可以检索 PropertyFileUserStorageProviderFactory.init () 中的配置:

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

    ...
}
Copy to Clipboard Toggle word wrap

7.4.2.2. 创建方法

创建提供程序工厂的最后一步是 create () 方法。

    @Override
    public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
        return new PropertyFileUserStorageProvider(session, model, properties);
    }
Copy to Clipboard Toggle word wrap

我们简单地分配 PropertyFileUserStorageProvider 类。这种创建方法将根据事务调用一次。

7.4.3. 打包和部署

我们提供程序实施的类文件应放在 jar 中。您还必须在 META-INF/services/org.keycloak.storage.UserStorageProviderFactory 文件中声明 provider factory 类。

org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
Copy to Clipboard Toggle word wrap

要部署此 jar,只需将它复制到 standalone/deployments/ 目录中。

7.4.4. 在 Admin 控制台中启用供应商

您可以在 Admin Console 的 User Federation 页面中启用用户存储供应商。

流程

  1. 从列表中选择刚才创建的供应商: readonly-property-file

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

  2. 单击 Save,因为我们没有任何配置。
  3. 返回到主 User Federation 页面

    您现在可以看到您的供应商列出。

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

返回顶部
Red Hat logoGithubredditYoutubeTwitter

学习

尝试、购买和销售

社区

关于红帽文档

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

让开源更具包容性

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

關於紅帽

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

Theme

© 2025 Red Hat