7.5. 简单的只读,查找示例
为了说明实施用户存储 SPI 的基础知识,让执行一个简单的示例。在本章中,您将看到一个简单的 UserStorageProvider 实施,用于在简单的属性文件中查找用户。属性文件包含用户名和密码定义,并且硬编码到 classpath 上的特定位置。提供程序将根据 ID 和用户名查找用户,并能够验证密码。源自此提供程序的用户将是只读的。
7.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;
}
此提供程序类的结构将存储对 KeycloakSession、componentModel 和属性文件的引用。我们稍后将使用所有这些内容。另请注意,已加载的用户映射。每当我们发现用户在此映射中时,我们都会避免在相同的事务中再次重新创建它。这是遵循许多供应商需要执行此操作(即,与 JPA 集成的任何供应商)的好做法。请记住,每个事务都会创建供应商类实例,并在事务完成后关闭。
7.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 的实现,将其存储在 加载的Users 中,以备将来参考,并返回此实例。
createAdapter () 方法使用帮助程序类 org.keycloak.storage.adapter.AbstractUserAdapter。这为 UserModel 提供基本实施。它使用用户的用户名作为外部 id,它根据所需的存储 id 格式自动生成用户 ID。
"f:" + component id + ":" + username
AbstractUserAdapter 的每个 get 方法返回 null 或空集合。但是,返回角色和组映射的方法将返回为每个用户配置的域的默认角色和组。AbstractUserAdapter 的每个集合都会抛出 org.keycloak.storage.ReadOnlyException。因此,如果您试图修改管理控制台中的用户,则会出现错误。
getUserById () 方法使用 org.keycloak.storage.StorageId helper 类解析 id 参数。调用 StorageId.getExternalId () 方法,以获取嵌入在 id 参数中的用户名。然后,该方法将委派给 getUserByUsername ()。
电子邮件不存储,因此 getUserByEmail () 方法返回 null。
7.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 表示密码有效。
7.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。
7.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 供应商类实施作为模板参数。在这里,我们指定之前定义的供应商类: PropertyFileUserStorageProvider。
如果没有指定 template 参数,您的供应商将无法正常工作。运行时执行类内省来确定提供程序实施 的功能接口。
getId () 方法标识运行时中的工厂,当您要为域启用用户存储供应商时,也将是管理控制台中显示的字符串。
7.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
我们可以指定用户属性文件的类路径,而不是硬编码它。然后您可以在 PropertyFileUserStorageProviderFactory.init () 中检索配置:
public void init(Config.Scope config) {
String path = config.get("path");
InputStream is = getClass().getClassLoader().getResourceAsStream(path);
...
}
7.5.2.2. 创建方法 复制链接链接已复制到粘贴板!
创建提供程序工厂的最后一步是 create () 方法。
@Override
public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
return new PropertyFileUserStorageProvider(session, model, properties);
}
我们只是分配 PropertyFileUserStorageProvider 类。这个创建方法将为每个事务调用一次。
7.5.3. 打包和部署 复制链接链接已复制到粘贴板!
我们提供程序实施的类文件应放在 jar 中。您还必须在 META-INF/services/org.keycloak.storage.UserStorageProviderFactory 文件中声明供应商工厂类。
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
要部署此 jar,将其复制到 provider/ 目录中,然后运行 bin/kc.[sh|bat] build。
7.5.4. 在管理门户中启用供应商 复制链接链接已复制到粘贴板!
您可以在管理控制台的 User Federation 页面中启用每个域的用户存储供应商。
用户联邦
流程
从列表中选择我们刚才创建的提供程序:
readonly-property-file。此时会显示我们的提供程序的配置页面。
点 Save,因为没有配置。
配置的供应商
返回到主 User Federation 页面
现在,您会看到列出了您的供应商。
用户联邦
现在,您将能够使用 users.properties 文件中声明的用户登录。此用户只能在登录后查看帐户页面。