为了覆盖 LDAP 连接并将密码验证重定向到我们自己的缓存密码系统,在 keycloak 中,每当 LDAP 连接丢失时。更简单的方法是围绕 LDAP 创建一个 HAProxy,以确保它永远不会宕机,但我们无权访问它,并且我们的客户端希望重定向到我们的缓存密码系统。
尽管如此,这篇文章的重点是讲述如何为 keycloak 创建自定义 LDAP 存储提供程序。
(检查keycloak文档 https://www.keycloak.org/docs/latest/server_development/index.html#_providers).
- 建筑
创建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
- 部署
这将展示如何部署到正在运行的独立 Keycloak 实例中。如果您要在 Docker 或嵌入式系统上部署 keycloak,则必须检查如何在那里部署 SPI。
将 fat jar 复制到独立部署文件夹中:
${KEYCLOAK_HOME}/standalone/deployments/
如果 keycloak 正在运行,则会出现一个 jar.ISDEPLOYNG 文件,如果一切顺利,则会出现一个 jar.DEPLOYED 文件。
这意味着如果您转到 keycloak 用户联合页面,您的自定义提供程序应该出现