Перейти к основному содержимому

Keycloak, встроенный в приложение Spring Boot

· 7 мин. чтения

Задача: Наибольшая подстрока без повторений

Для заданной строки s, найдите длину наибольшей подстроки без повторяющихся символов. Подстрока — это непрерывная непустая последовательность символов внутри строки...

ANDROMEDA 42

1. Обзор

Keycloak — это решение для управления идентификацией и доступом с открытым исходным кодом, администрируемое RedHat и разработанное на Java компанией JBoss.

В этом руководстве мы узнаем , как настроить сервер Keycloak, встроенный в приложение Spring Boot . Это упрощает запуск предварительно настроенного сервера Keycloak.

Keycloak также можно запустить как автономный сервер , но тогда потребуется загрузить его и настроить через консоль администратора.

2. Предварительная настройка Keycloak

Для начала давайте разберемся, как мы можем предварительно настроить сервер Keycloak.

Сервер содержит набор областей, каждая из которых действует как изолированная единица для управления пользователями. Чтобы предварительно настроить его, нам нужно указать файл определения области в формате JSON.

Все, что можно настроить с помощью консоли администратора Keycloak , сохраняется в этом JSON.

Наш сервер авторизации будет предварительно настроен с помощью foreach-realm.json . Давайте посмотрим несколько соответствующих конфигураций в файле:

  • пользователи : нашими пользователями по умолчанию будут john@test.com и mike@other.com ; они также будут иметь свои учетные данные здесь
  • клиенты : мы определим клиента с идентификатором newClient
  • standardFlowEnabled : установите значение true, чтобы активировать поток кода авторизации для newClient.
  • redirectUris : здесь перечислены URL-адреса newClient , на которые сервер будет перенаправляться после успешной аутентификации .
  • webOrigins : установите значение «+» , чтобы включить поддержку CORS для всех URL-адресов, перечисленных как redirectUris .

Сервер Keycloak выдает токены JWT по умолчанию, поэтому для этого не требуется отдельная настройка. Далее рассмотрим конфигурации Maven.

3. Конфигурация Maven

Поскольку мы будем встраивать Keycloak в приложение Spring Boot, нет необходимости загружать его отдельно.

Вместо этого мы настроим следующий набор зависимостей :

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>

Обратите внимание, что здесь мы используем версию Spring Boot 2.6.7. Зависимости spring-boot-starter-data-jpa и H2 были добавлены для сохранения. Другие зависимости springframework.boot предназначены для веб-поддержки, поскольку нам также необходимо иметь возможность запускать сервер авторизации Keycloak, а также консоль администратора в качестве веб-служб.

Нам также понадобится пара зависимостей для Keycloak и RESTEasy :

<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
<version>3.15.1.Final</version>
</dependency>

<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-dependencies-server-all</artifactId>
<version>18.0.0</version>
<type>pom</type>
</dependency>

Проверьте сайт Maven на наличие последних версий Keycloak и RESTEasy .

И, наконец, мы должны переопределить свойство <infinispan.version> , чтобы использовать версию, объявленную Keycloak, вместо той, которая определена Spring Boot:

<properties>
<infinispan.version>13.0.8.Final</infinispan.version>
</properties>

4. Встроенная конфигурация Keycloak

Теперь давайте определим конфигурацию Spring для нашего сервера авторизации:

@Configuration
public class EmbeddedKeycloakConfig {

@Bean
ServletRegistrationBean keycloakJaxRsApplication(
KeycloakServerProperties keycloakServerProperties, DataSource dataSource) throws Exception {

mockJndiEnvironment(dataSource);
EmbeddedKeycloakApplication.keycloakServerProperties = keycloakServerProperties;
ServletRegistrationBean servlet = new ServletRegistrationBean<>(
new HttpServlet30Dispatcher());
servlet.addInitParameter("javax.ws.rs.Application",
EmbeddedKeycloakApplication.class.getName());
servlet.addInitParameter(ResteasyContextParameters.RESTEASY_SERVLET_MAPPING_PREFIX,
keycloakServerProperties.getContextPath());
servlet.addInitParameter(ResteasyContextParameters.RESTEASY_USE_CONTAINER_FORM_PARAMS,
"true");
servlet.addUrlMappings(keycloakServerProperties.getContextPath() + "/*");
servlet.setLoadOnStartup(1);
servlet.setAsyncSupported(true);
return servlet;
}

@Bean
FilterRegistrationBean keycloakSessionManagement(
KeycloakServerProperties keycloakServerProperties) {
FilterRegistrationBean filter = new FilterRegistrationBean<>();
filter.setName("Keycloak Session Management");
filter.setFilter(new EmbeddedKeycloakRequestFilter());
filter.addUrlPatterns(keycloakServerProperties.getContextPath() + "/*");

return filter;
}

private void mockJndiEnvironment(DataSource dataSource) throws NamingException {
NamingManager.setInitialContextFactoryBuilder(
(env) -> (environment) -> new InitialContext() {
@Override
public Object lookup(Name name) {
return lookup(name.toString());
}

@Override
public Object lookup(String name) {
if ("spring/datasource".equals(name)) {
return dataSource;
} else if (name.startsWith("java:jboss/ee/concurrency/executor/")) {
return fixedThreadPool();
}
return null;
}

@Override
public NameParser getNameParser(String name) {
return CompositeName::new;
}

@Override
public void close() {
}
});
}

@Bean("fixedThreadPool")
public ExecutorService fixedThreadPool() {
return Executors.newFixedThreadPool(5);
}

@Bean
@ConditionalOnMissingBean(name = "springBootPlatform")
protected SimplePlatformProvider springBootPlatform() {
return (SimplePlatformProvider) Platform.getPlatform();
}
}

Примечание: не беспокойтесь об ошибке компиляции, позже мы определим класс EmbeddedKeycloakRequestFilter .

Как мы видим здесь, мы сначала настроили Keycloak как приложение JAX-RS с KeycloakServerProperties для постоянного хранения свойств Keycloak, как указано в нашем файле определения области. Затем мы добавили фильтр управления сеансом и смоделировали среду JNDI, чтобы использовать spring/datasource , которая является нашей базой данных H2 в памяти.

5. Свойства KeycloakServer

Теперь давайте посмотрим на KeycloakServerProperties , которые мы только что упомянули:

@ConfigurationProperties(prefix = "keycloak.server")
public class KeycloakServerProperties {
String contextPath = "/auth";
String realmImportFile = "foreach-realm.json";
AdminUser adminUser = new AdminUser();

// getters and setters

public static class AdminUser {
String username = "admin";
String password = "admin";

// getters and setters
}
}

Как мы видим, это простой POJO для установки contextPath , adminUser и файла определения области .

6. Приложение EmbeddedKeycloak

Далее давайте посмотрим на класс, который использует настройки, которые мы установили ранее, для создания областей:

public class EmbeddedKeycloakApplication extends KeycloakApplication {
private static final Logger LOG = LoggerFactory.getLogger(EmbeddedKeycloakApplication.class);
static KeycloakServerProperties keycloakServerProperties;

protected void loadConfig() {
JsonConfigProviderFactory factory = new RegularJsonConfigProviderFactory();
Config.init(factory.create()
.orElseThrow(() -> new NoSuchElementException("No value present")));
}

@Override
protected ExportImportManager bootstrap() {
final ExportImportManager exportImportManager = super.bootstrap();
createMasterRealmAdminUser();
createForEachRealm();
return exportImportManager;
}

private void createMasterRealmAdminUser() {
KeycloakSession session = getSessionFactory().create();
ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session);
AdminUser admin = keycloakServerProperties.getAdminUser();
try {
session.getTransactionManager().begin();
applianceBootstrap.createMasterRealmUser(admin.getUsername(), admin.getPassword());
session.getTransactionManager().commit();
} catch (Exception ex) {
LOG.warn("Couldn't create keycloak master admin user: {}", ex.getMessage());
session.getTransactionManager().rollback();
}
session.close();
}

private void createForEachRealm() {
KeycloakSession session = getSessionFactory().create();
try {
session.getTransactionManager().begin();
RealmManager manager = new RealmManager(session);
Resource lessonRealmImportFile = new ClassPathResource(
keycloakServerProperties.getRealmImportFile());
manager.importRealm(JsonSerialization.readValue(lessonRealmImportFile.getInputStream(),
RealmRepresentation.class));
session.getTransactionManager().commit();
} catch (Exception ex) {
LOG.warn("Failed to import Realm json file: {}", ex.getMessage());
session.getTransactionManager().rollback();
}
session.close();
}
}

7. Пользовательские реализации платформы

Как мы уже говорили, Keycloak разработан RedHat/JBoss. Поэтому он предоставляет функциональные возможности и библиотеки расширений для развертывания приложения на сервере Wildfly или в качестве решения Quarkus.

В данном случае мы отходим от этих альтернатив, и, как следствие, нам приходится предоставлять собственные реализации для некоторых интерфейсов и классов, зависящих от платформы.

Например, в EmbeddedKeycloakApplication , который мы только что настроили, мы сначала загрузили конфигурацию сервера Keycloak keycloak-server.json , используя пустой подкласс абстрактного JsonConfigProviderFactory :

public class RegularJsonConfigProviderFactory extends JsonConfigProviderFactory { }

Затем мы расширили KeycloakApplication для создания двух областей: master и foreach . Они создаются в соответствии со свойствами, указанными в нашем файле определения области, foreach-realm.json .

Как видите, мы используем KeycloakSession для выполнения всех транзакций, и для того, чтобы это работало должным образом, нам пришлось создать собственный AbstractRequestFilter ( EmbeddedKeycloakRequestFilter ) и настроить bean-компонент для этого с помощью KeycloakSessionServletFilter в файле EmbeddedKeycloakConfig .

Кроме того, нам нужна пара пользовательских провайдеров, чтобы у нас были собственные реализации org.keycloak.common.util.ResteasyProvider и org.keycloak.platform.PlatformProvider и мы не полагались на внешние зависимости.

Важно отметить, что информация об этих настраиваемых поставщиках должна быть включена в папку META-INF/services проекта, чтобы они были доступны во время выполнения.

8. Объединяем все вместе

Как мы видели, Keycloak значительно упростил необходимые настройки со стороны приложения . Нет необходимости программно определять источник данных или какие-либо конфигурации безопасности.

Чтобы собрать все это вместе, нам нужно определить конфигурацию для Spring и приложения Spring Boot.

8.1. приложение.yml

Мы будем использовать простой YAML для конфигураций Spring:

server:
port: 8083

spring:
datasource:
username: sa
url: jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE

keycloak:
server:
contextPath: /auth
adminUser:
username: bael-admin
password: ********
realmImportFile: foreach-realm.json

8.2. Весеннее загрузочное приложение

Наконец, вот приложение Spring Boot:

@SpringBootApplication(exclude = LiquibaseAutoConfiguration.class)
@EnableConfigurationProperties(KeycloakServerProperties.class)
public class AuthorizationServerApp {
private static final Logger LOG = LoggerFactory.getLogger(AuthorizationServerApp.class);

public static void main(String[] args) throws Exception {
SpringApplication.run(AuthorizationServerApp.class, args);
}

@Bean
ApplicationListener<ApplicationReadyEvent> onApplicationReadyEventListener(
ServerProperties serverProperties, KeycloakServerProperties keycloakServerProperties) {
return (evt) -> {
Integer port = serverProperties.getPort();
String keycloakContextPath = keycloakServerProperties.getContextPath();
LOG.info("Embedded Keycloak started: http://localhost:{}{} to use keycloak",
port, keycloakContextPath);
};
}
}

Примечательно, что здесь мы включили конфигурацию KeycloakServerProperties , чтобы внедрить ее в bean- компонент ApplicationListener .

После запуска этого класса мы можем получить доступ к приветственной странице сервера авторизации по адресу http://localhost:8083/auth/ .

8.3. Исполняемый JAR-файл

Мы также можем создать исполняемый файл jar для упаковки и запуска приложения:

<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.foreach.auth.AuthorizationServerApp</mainClass>
<requiresUnpack>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-jpa</artifactId>
</dependency>
</requiresUnpack>
</configuration>
</plugin>

Здесь мы указали основной класс, а также поручили Maven распаковать некоторые зависимости Keycloak. Это распаковывает библиотеки из толстых банок во время выполнения, и теперь мы можем запустить приложение с помощью стандартной команды java -jar <имя-артефакта> .

Страница приветствия сервера авторизации теперь доступна, как показано ранее.

9. Заключение

В этом кратком руководстве мы увидели, как настроить сервер Keycloak, встроенный в приложение Spring Boot. Исходный код этого приложения доступен на GitHub .

Первоначальная идея этой реализации была разработана Томасом Даримонтом и может быть найдена в проекте embedded-spring-boot-keycloak-server .