#java #maven #ldap #keycloak
#java #maven #ldap #keycloak
Вопрос:
Чтобы переопределить соединение LDAP и перенаправить проверку пароля в нашу собственную систему кэшированных паролей, в keycloak, всякий раз, когда соединение LDAP теряется. Более простым способом было создать HAProxy вокруг LDAP, чтобы гарантировать, что он никогда не выйдет из строя, но у нас нет доступа к этому, и наш клиент хочет перенаправить на нашу систему кэшированных паролей. Тем не менее, смысл этого поста в том, чтобы рассказать, как создать пользовательский поставщик хранилища LDAP для keycloak.
(проверьте документацию keycloak).
- Построение
Создайте Java-проект (jar) и добавьте следующие зависимости
pom.xml (для maven! при использовании gradle необходимо также добавить эти зависимости)
Примечание: убедитесь, что версия зависимостей keycloak совпадает с версией запущенного экземпляра keycloak, также обратите внимание, что эти зависимости имеют значение scope=provided, что означает, что они уже будут в загрузчике классов.
<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, поскольку нам также понадобятся все эти функции, и просто нужно настроить несколько методов.
Пользовательский ldapstorageprovider расширит 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
}
}
The CustomLDAPStorageProviderFactory will extend LDAPStorageProviderFactory and will be loaded only once and will be always the same instance throughout the keycloak uptime.
Note: The getId() will display the name of this provider in the keycloak user federation area. The create() must be overridden in order to instantiate our custom provider. I also had to override the getConfigProperties() because, although all the configs were showing up in the federation manager there were no labels on the HTML boxes and this was a workaround for now but I guess it has something to do with config decorators. need to check that in the future.
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;
}
}
Next we have to tell Kecloak that we have a new User Storage Provider and this is done by adding to our project the following file.
в src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory (создайте этот файл и добавьте следующую строку)
foo.bar.lion.storage.ldap.CustomLDAPStorageProviderFactory
Нам также нужно добавить файл Jboss, чтобы указать, от чего мы зависим, поэтому мы создаем этот файл:
в src/main/resources/META-INF/jboss-deployment-structure.xml
Обратите внимание, что неправильные зависимости здесь могут привести к (Вызванному: «java.util.Ошибка ServiceConfigurationError»: org.keycloak.foo.bar: org.keycloak.foo.Abar не является подтипом)
<?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. Если вы собираетесь развернуть keycloak в docker или встроенной системе, вам нужно будет проверить, как развернуть SPI там.
Скопируйте ваш fat jar в папку автономных развертываний:
$ {KEYCLOAK_HOME}/standalone/deployments/
если keycloak запущен, jar.Появится файл ISDEPLOYNG и jar.РАЗВЕРНУТО, если все прошло хорошо.
это означает, что если вы перейдете на страницу федерации пользователей keycloak, должен появиться ваш пользовательский поставщик
Комментарии:
1. Вы, наконец, решили проблему с HTML-метками?
2. Привет, извините, но я еще не понял. это был PoC, и не было необходимости разбираться в этом.
3. попробуйте вызвать super.getConfigProperties()