如何在 Keycloak 上创建自定义 UserStorageSPI

2024-02-29

为了覆盖 LDAP 连接并将密码验证重定向到我们自己的缓存密码系统,在 keycloak 中,每当 LDAP 连接丢失时。更简单的方法是围绕 LDAP 创建一个 HAProxy,以确保它永远不会宕机,但我们无权访问它,并且我们的客户端希望重定向到我们的缓存密码系统。 尽管如此,这篇文章的重点是讲述如何为 keycloak 创建自定义 LDAP 存储提供程序。

(检查keycloak文档 https://www.keycloak.org/docs/latest/server_development/index.html#_providers).

  1. 建筑

创建Java项目(jar)并添加以下依赖

pom.xml(对于 Maven!如果使用 gradle,还必须添加这些依赖项)

注意:确保依赖项的 keycloak 版本与正在运行的 keycloak 实例相同,另请注意,这些依赖项是范围=提供这意味着这些已经在类加载器中了。

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>foo.bar</groupId>
<artifactId>custom-ldap-spi</artifactId>
<version>1.0.0</version>

<name>Custom LDAP Provider</name>
<description />
<packaging>jar</packaging>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <keycloak.version>11.0.2</keycloak.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.keycloak</groupId>
        <artifactId>keycloak-core</artifactId>
        <version>${keycloak.version}</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.keycloak</groupId>
        <artifactId>keycloak-server-spi</artifactId>
        <version>${keycloak.version}</version>
                    <scope>provided</scope>
        
    </dependency>
    <dependency>
        <groupId>org.keycloak</groupId>
        <artifactId>keycloak-server-spi-private</artifactId>
        <version>${keycloak.version}</version>
                    <scope>provided</scope>
        
    </dependency>
    <dependency>
        <groupId>org.keycloak</groupId>
        <artifactId>keycloak-kerberos-federation</artifactId>
        <version>${keycloak.version}</version>
                    <scope>provided</scope>
        
    </dependency>
    <dependency>
        <groupId>org.keycloak</groupId>
        <artifactId>keycloak-ldap-federation</artifactId>
        <version>${keycloak.version}</version>
                    <scope>provided</scope>
        
    </dependency>
    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-jaxrs</artifactId>
        <version>3.9.0.Final</version>
                    <scope>provided</scope>
        
    </dependency>
    <dependency>
        <groupId>org.jboss.logging</groupId>
        <artifactId>jboss-logging</artifactId>
        <version>3.4.1.Final</version>
                    <scope>provided</scope>
        
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
                    <scope>provided</scope>
        
    </dependency>
    <dependency>
        <groupId>org.jboss.spec.javax.transaction</groupId>
        <artifactId>jboss-transaction-api_1.2_spec</artifactId>
        <version>1.1.1.Final</version>
                    <scope>provided</scope>
        
    </dependency>
    
    <dependency>
        <groupId>org.jboss.spec.javax.ejb</groupId>
        <artifactId>jboss-ejb-api_3.2_spec</artifactId>
        <version>2.0.0.Final</version>
    </dependency>
</dependencies>
<build>
    <finalName>custom-ldap-provider</finalName>
    <plugins>
        <plugin>
            <artifactId>maven-assembly-plugin</artifactId>
            <configuration>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
            </configuration>
        </plugin>
    </plugins>

</build>

接下来,我们需要添加两个类来扩展现有的 LDAP 提供程序,因为我们也需要所有这些功能,并且只需要调整一些方法。

CustomLDAPStorageProvider 将扩展 LDAPStorageProvider 并如下所示:

注意:这些重写的方法用于测试和调试,目前这里没有业务逻辑,稍后我将根据需要添加。另请注意,在 LDAP 密码验证异常中,我们只是说“真实”密码有效。在这里我们将调用我们自己的密码验证器。

package foo.bar.lion.storage.ldap;

import javax.ejb.Remove;

import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;

public class CustomLDAPStorageProvider extends LDAPStorageProvider {

private static final Logger logger = Logger.getLogger(CustomLDAPStorageProvider.class);

public CustomLDAPStorageProvider(CustomLDAPStorageProviderFactory factory, KeycloakSession session,
        ComponentModel model, LDAPIdentityStore ldapIdentityStore) {
    super(factory, session, model, ldapIdentityStore);
}

@Override
public UserModel validate(RealmModel realm, UserModel local) {
    try {
        logger.error("#######   VALIDATE USER");

        return super.validate(realm, local);
    } catch (Exception e) {
        logger.error("####### ERROR   VALIDATE USER  ");

        logger.error(e);
        return null;
    }
}

@Override
public boolean validPassword(RealmModel realm, UserModel user, String password) {
    try {
        logger.error("#######   VALIDATE password");
        return super.validPassword(realm, user, password);
    } catch (Exception e) {
        logger.error("####### FOR DEMO PURPOUSE ONLY PASSWORDS WILL ALLWAYS BE CORRECT");

        return true;
    }
}

@Override
protected LDAPObject loadAndValidateUser(RealmModel realm, UserModel local) {
    try {
        logger.error("#######   LOAD AND VALIDATE USER ");
        return super.loadAndValidateUser(realm, local);

    } catch (Exception e) {
        logger.error("####### Error LOAD AND VALIDATE USER  ");

        LDAPObject cached = new LDAPObject();
        cached.setUuid(local.getId());
        return cached;
    }
}

@Override
public LDAPObject loadLDAPUserByUsername(RealmModel realm, String username) {
    logger.error("####### LOAD BY USER MANE  " + username);
    LDAPObject user = super.loadLDAPUserByUsername(realm, username);

    return user;
}

@Remove
@Override
public void close() {
    // according to
    // https://www.keycloak.org/docs/latest/server_development/#leveraging-java-ee
}

}

CustomLDAPStorageProviderFactory 将扩展 LDAPStorageProviderFactory,并且仅加载一次,并且在整个 keycloak 正常运行时间内始终是同一实例。

注意: getId() 将在 keycloak 用户联合区域中显示该提供者的名称。必须重写 create() 才能实例化我们的自定义提供程序。我还必须重写 getConfigProperties(),因为虽然所有配置都显示在联合管理器中,但 HTML 框中没有标签,这是目前的解决方法,但我猜它与配置装饰器有关。将来需要检查一下。

package foo.bar.lion.storage.ldap;

import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.keycloak.Config;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ServerInfoAwareProviderFactory;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.ldap.LDAPIdentityStoreRegistry;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;

public class CustomLDAPStorageProviderFactory extends LDAPStorageProviderFactory
        implements ServerInfoAwareProviderFactory {
private LDAPIdentityStoreRegistry ldapStoreRegistry;

@Override
public String getId() {
    return "lion-ldap";
}

@Override
public void init(Config.Scope config) {
    this.ldapStoreRegistry = new LDAPIdentityStoreRegistry();
}

@Override
public LDAPStorageProvider create(KeycloakSession session, ComponentModel model) {
    Map<ComponentModel, LDAPConfigDecorator> configDecorators = getLDAPConfigDecorators(session, model);

    LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(session, model, configDecorators);
    return new CustomLDAPStorageProvider(this, session, model, ldapIdentityStore);
}

@Override
public List<ProviderConfigProperty> getConfigProperties() {
    List<ProviderConfigProperty> props = new LinkedList<>();
    props.add(new ProviderConfigProperty(LDAPConstants.EDIT_MODE, LDAPConstants.EDIT_MODE, LDAPConstants.EDIT_MODE,
            ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(UserStorageProviderModel.IMPORT_ENABLED,
            UserStorageProviderModel.IMPORT_ENABLED, UserStorageProviderModel.IMPORT_ENABLED,
            ProviderConfigProperty.BOOLEAN_TYPE, "true"));
    props.add(new ProviderConfigProperty(LDAPConstants.SYNC_REGISTRATIONS, LDAPConstants.SYNC_REGISTRATIONS,
            LDAPConstants.SYNC_REGISTRATIONS, ProviderConfigProperty.BOOLEAN_TYPE, "false"));
    props.add(new ProviderConfigProperty(LDAPConstants.VENDOR, LDAPConstants.VENDOR, LDAPConstants.VENDOR,
            ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.USE_PASSWORD_MODIFY_EXTENDED_OP,
            LDAPConstants.USE_PASSWORD_MODIFY_EXTENDED_OP, LDAPConstants.USE_PASSWORD_MODIFY_EXTENDED_OP,
            ProviderConfigProperty.BOOLEAN_TYPE, "false"));
    props.add(
            new ProviderConfigProperty(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, LDAPConstants.USERNAME_LDAP_ATTRIBUTE,
                    LDAPConstants.USERNAME_LDAP_ATTRIBUTE, ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.RDN_LDAP_ATTRIBUTE, LDAPConstants.RDN_LDAP_ATTRIBUTE,
            LDAPConstants.RDN_LDAP_ATTRIBUTE, ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.UUID_LDAP_ATTRIBUTE, LDAPConstants.UUID_LDAP_ATTRIBUTE,
            LDAPConstants.UUID_LDAP_ATTRIBUTE, ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.USER_OBJECT_CLASSES, LDAPConstants.USER_OBJECT_CLASSES,
            LDAPConstants.USER_OBJECT_CLASSES, ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_URL, LDAPConstants.CONNECTION_URL,
            LDAPConstants.CONNECTION_URL, ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.USERS_DN, LDAPConstants.USERS_DN, LDAPConstants.USERS_DN,
            ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.AUTH_TYPE, LDAPConstants.AUTH_TYPE, LDAPConstants.AUTH_TYPE,
            ProviderConfigProperty.STRING_TYPE, "simple"));
    props.add(new ProviderConfigProperty(LDAPConstants.START_TLS, LDAPConstants.START_TLS, LDAPConstants.START_TLS,
            ProviderConfigProperty.BOOLEAN_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.BIND_DN, LDAPConstants.BIND_DN, LDAPConstants.BIND_DN,
            ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.BIND_CREDENTIAL, LDAPConstants.BIND_CREDENTIAL,
            LDAPConstants.BIND_CREDENTIAL, ProviderConfigProperty.PASSWORD, "", true));
    props.add(new ProviderConfigProperty(LDAPConstants.CUSTOM_USER_SEARCH_FILTER,
            LDAPConstants.CUSTOM_USER_SEARCH_FILTER, LDAPConstants.CUSTOM_USER_SEARCH_FILTER,
            ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.SEARCH_SCOPE, LDAPConstants.SEARCH_SCOPE,
            LDAPConstants.SEARCH_SCOPE, ProviderConfigProperty.STRING_TYPE, "1"));
    props.add(new ProviderConfigProperty(LDAPConstants.VALIDATE_PASSWORD_POLICY,
            LDAPConstants.VALIDATE_PASSWORD_POLICY, LDAPConstants.VALIDATE_PASSWORD_POLICY,
            ProviderConfigProperty.BOOLEAN_TYPE, "false"));
    props.add(new ProviderConfigProperty(LDAPConstants.TRUST_EMAIL, LDAPConstants.TRUST_EMAIL,
            LDAPConstants.TRUST_EMAIL, ProviderConfigProperty.BOOLEAN_TYPE, "false"));
    props.add(new ProviderConfigProperty(LDAPConstants.USE_TRUSTSTORE_SPI, LDAPConstants.USE_TRUSTSTORE_SPI,
            LDAPConstants.USE_TRUSTSTORE_SPI, ProviderConfigProperty.STRING_TYPE, "ldapsOnly"));
    props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING, LDAPConstants.CONNECTION_POOLING,
            LDAPConstants.CONNECTION_POOLING, ProviderConfigProperty.BOOLEAN_TYPE, "true"));
    props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING_AUTHENTICATION,
            LDAPConstants.CONNECTION_POOLING_AUTHENTICATION, LDAPConstants.CONNECTION_POOLING_AUTHENTICATION,
            ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING_DEBUG,
            LDAPConstants.CONNECTION_POOLING_DEBUG, LDAPConstants.CONNECTION_POOLING_DEBUG,
            ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING_INITSIZE,
            LDAPConstants.CONNECTION_POOLING_INITSIZE, LDAPConstants.CONNECTION_POOLING_INITSIZE,
            ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING_MAXSIZE,
            LDAPConstants.CONNECTION_POOLING_MAXSIZE, LDAPConstants.CONNECTION_POOLING_MAXSIZE,
            ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING_PREFSIZE,
            LDAPConstants.CONNECTION_POOLING_PREFSIZE, LDAPConstants.CONNECTION_POOLING_PREFSIZE,
            ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING_PROTOCOL,
            LDAPConstants.CONNECTION_POOLING_PROTOCOL, LDAPConstants.CONNECTION_POOLING_PROTOCOL,
            ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING_TIMEOUT,
            LDAPConstants.CONNECTION_POOLING_TIMEOUT, LDAPConstants.CONNECTION_POOLING_TIMEOUT,
            ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_TIMEOUT, LDAPConstants.CONNECTION_TIMEOUT,
            LDAPConstants.CONNECTION_TIMEOUT, ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.READ_TIMEOUT, LDAPConstants.READ_TIMEOUT,
            LDAPConstants.READ_TIMEOUT, ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(LDAPConstants.PAGINATION, LDAPConstants.PAGINATION,
            LDAPConstants.PAGINATION, ProviderConfigProperty.BOOLEAN_TYPE, "true"));
    props.add(new ProviderConfigProperty(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION,
            KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION,
            ProviderConfigProperty.BOOLEAN_TYPE, "false"));
    props.add(new ProviderConfigProperty(KerberosConstants.SERVER_PRINCIPAL, KerberosConstants.SERVER_PRINCIPAL,
            KerberosConstants.SERVER_PRINCIPAL, ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(KerberosConstants.KEYTAB, KerberosConstants.KEYTAB,
            KerberosConstants.KEYTAB, ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(KerberosConstants.KERBEROS_REALM, KerberosConstants.KERBEROS_REALM,
            KerberosConstants.KERBEROS_REALM, ProviderConfigProperty.STRING_TYPE, ""));
    props.add(new ProviderConfigProperty(KerberosConstants.DEBUG, KerberosConstants.DEBUG, KerberosConstants.DEBUG,
            ProviderConfigProperty.BOOLEAN_TYPE, "false"));
    props.add(new ProviderConfigProperty(KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION,
            KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION,
            KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION, ProviderConfigProperty.BOOLEAN_TYPE,
            "false"));
    props.add(new ProviderConfigProperty(KerberosConstants.SERVER_PRINCIPAL, KerberosConstants.SERVER_PRINCIPAL,
            KerberosConstants.SERVER_PRINCIPAL, ProviderConfigProperty.STRING_TYPE, ""));

    return props;
}

@Override
public Map<String, String> getOperationalInfo() {
    Map<String, String> ret = new LinkedHashMap<>();
    ret.put("custom-ldap", "lion-ldap");
    return ret;
}

}

接下来,我们必须告诉 Kecloak 我们有一个新的用户存储提供程序,这是通过将以下文件添加到我们的项目中来完成的。

在 src/main/resources/META-INF/services/ 中org.keycloak.storage.UserStorageProviderFactory(创建此文件并添加下一行)

foo.bar.lion.storage.ldap.CustomLDAPStorageProviderFactory

我们还需要添加一个 Jboss 文件来告诉我们依赖什么,所以我们创建这个文件:

在 src/main/resources/META-INF/ 中jboss-部署-结构.xml

请注意,这里不正确的依赖关系可能会导致 (Caused by: "java.util.ServiceConfigurationError": org.keycloak.foo.bar: org.keycloak.foo.Abar not a subtype)

<?xml version="1.0" encoding="UTF-8"?>
<jboss-deployment-structure>
    <deployment>
        <dependencies>

            <module name="org.keycloak.keycloak-core" />
            <module name="org.keycloak.keycloak-server-spi" />
            <module name="org.keycloak.keycloak-server-spi-private" />
            <module name="org.keycloak.keycloak-kerberos-federation" />
            <module name="org.keycloak.keycloak-ldap-federation" />
            <module name="org.keycloak.keycloak-model-jpa" />
            <module name="org.keycloak.keycloak-common" />
            <module name="org.keycloak.keycloak-model-infinispan" />
            <module name="org.keycloak.keycloak-services" />
        </dependencies>
    </deployment>
</jboss-deployment-structure>

1.1 编译

我只使用 fat Jar 进行了测试,以便将外部依赖项嵌入到 jar 中。为此,在 Maven 上运行以下命令:

请注意,您必须有用于组装的插件。

$ mvn clean install assembly:single
  1. 部署

这将展示如何部署到正在运行的独立 Keycloak 实例中。如果您要在 Docker 或嵌入式系统上部署 keycloak,则必须检查如何在那里部署 SPI。

将 fat jar 复制到独立部署文件夹中:

${KEYCLOAK_HOME}/standalone/deployments/

如果 keycloak 正在运行,则会出现一个 jar.ISDEPLOYNG 文件,如果一切顺利,则会出现一个 jar.DEPLOYED 文件。

这意味着如果您转到 keycloak 用户联合页面,您的自定义提供程序应该出现


None

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何在 Keycloak 上创建自定义 UserStorageSPI 的相关文章

随机推荐