В современном мире веб-разработки обеспечение безопасности пользовательских идентификаторов и управление доступом к ресурсам становятся все более важными задачами. Один из мощных инструментов, предоставляющих полноценное решение для этих задач, это Keycloak, современная система управления идентичностью и доступом.
Keycloak - это средство с открытым исходным кодом, предоставляющее полнофункциональную платформу для управления идентичностью и доступом. Он поддерживает различные стандарты безопасности, включая OAuth 2.0, OpenID Connect и другие, что делает его идеальным выбором для современных веб-приложений.
В данной статье мы рассмотрим процесс интеграции Keycloak в наше приложение Spring Boot 3 в качестве сервера авторизации с использованием протокола OAuth2. Обсудим смысл OAuth2, его механизм работы и сравним его с другими протоколами. Кроме того, мы настроим Keycloak с использованием Docker Compose, воспользовавшись PostgreSQL в качестве базы данных для Keycloak. Затем мы интегрируем Keycloak с нашим приложением Spring Boot 3, используя протокол OAuth2. Также мы подключим Keycloak Admin Client и, наконец, проверим функциональность всей системы.

OAuth 2.0: Механизм работы OAuth2 и его структура
OAuth2 является протоколом авторизации, предназначенным для делегирования доступа к ресурсам от имени пользователя без передачи ему его учетных данных. Протокол поддерживает различные потоки (grant types), включая Authorization Code, Implicit, Resource Owner Password Credentials и Client Credentials, что делает его гибким для различных сценариев использования. Основная идея OAuth2 заключается в том, чтобы позволить пользователям предоставлять доступ к своим данным на сторонних ресурсах, не раскрывая свои учетные данные. Вместо этого, для аутентификации используются токены, которые выдаются после успешной авторизации. Такими токенами являются Access Token (Токен доступа) и Refresh Token (Токен обновления). Access токен предоставляет приложению временный доступ к ресурсам на сервере. Он имеет ограниченный срок действия, чаще всего короткий (несколько минут или часов). Access токен передается при каждом запросе к защищенному ресурсу. Refresh токен используется для обновления Access токена после его истечения срока действия. У Refresh токена обычно более длительный срок действия, чем у Access токена. Он предназначен для длительного использования. Когда Access токен истекает, приложение отправляет Refresh токен на сервер авторизации и сервер возвращает новый Access токен. В контексте OAuth2 существуют специальные серверы авторизации, также известные как Authorization Servers. Эти серверы играют ключевую роль в процессе выдачи токенов и управлении доступом. Различные веб-сервисы, такие как Google, Facebook, GitHub и другие, являются примерами таких серверов. Основными компонентами механизма OAuth 2 являются:
- Resource Owner (Владелец ресурса): Пользователь, владеющий данными (ресурсами), к которым запрашивается доступ. 
- Client (Клиент): Приложение или сервис, запрашивающий доступ к ресурсам у владельца. 
- Authorization Server (Сервер авторизации): Сервер, управляющий процессом авторизации и выдачей токенов. 
- Resource Server (Сервер ресурсов): Сервер, управляющий защищенными ресурсами, к которым запрашивается доступ. 
Механизм работы OAuth2

1. Регистрация приложения (Client Registration):
Клиент (ваше веб-приложение или сервис) должен быть предварительно зарегистрирован на сервере авторизации. В этот момент вы получаете идентификатор клиента (Client ID) и секрет (Client Secret), которые используются для аутентификации вашего приложения.
2. Для получения доступ к защищенному ресурсу веб-приложение направляет пользователя на страницу авторизации Keycloak.
3. Подтверждение авторизации (User Authentication):
Пользователь вводит свои учетные данные на странице сервера авторизации и подтверждает предоставление доступа вашему приложению.
4․ Получение кода авторизации (Authorization Code):
После успешной аутентификации сервер авторизации направляет пользователя обратно на ваш сайт по указанному вами URL-перенаправления, при этом включается код авторизации.
5․ Обмен кода на токены (Token Exchange):
Клиент использует полученный код для запроса Access Token и, при необходимости, Refresh Token у сервера авторизации. Запрос выглядит примерно так:
6․ Выдача токенов (Token Issuance):
После успешной проверки кода сервер авторизации отвечает JSON-объектом, который содержит Access Token и, возможно, Refresh Token.
7. Доступ к ресурсам (Access Resources):
Клиент использует полученный Access Token для доступа к защищенным ресурсам, отправляя его в заголовке запроса.
8․ Обновление токена (Token Refresh):
При истечении срока действия Access Token клиент может использовать Refresh Token для получения нового Access Token без повторной аутентификации владельца ресурса.
Сравнение Oauth2 с другими подходами авторизации
| Критерий | OAuth 2.0 | OpenID Connect | SAML | Basic | 
| Передача пароля | Не рекомендуется, используются маркеры доступа (Access tokens) 
 | Может предоставлять аутентификацию, включая передачу пароля | Используется для передачи атрибутов, включая аутентификацию | Основной метод, небезопасен при передаче через сеть | 
| Передача ролей | Возможность передачи различных разрешений. Например, чтение данных, запись данных, выполнение определенных действий и т. д. | Может передавать информацию о ролях через атрибуты | Специализируется на передаче атрибутов, включая роли | Ограничен в передаче дополнительной информации о ролях | 
| Простота реализации | Относительно простая реализация | Может быть более сложным из-за дополнительных функциональных возможностей | Может быть более сложным в сравнении с OAuth 2.0 | Прост в реализации, но ограничен функциональностью | 
| Поддержка устройств | Поддерживает различные сценарии, включая мобильные устройства и веб-приложения | Поддерживает различные сценарии, но может быть более сложным в реализации | Традиционно ориентирован на веб-приложения | Ограничен в поддержке современных сценариев использования | 
| Расширяемость | Позволяет расширять функциональность через дополнительные спецификации и профили | Предоставляет расширенные возможности для удовлетворения специфических требований | Предоставляет возможность определения дополнительных профилей и расширений | Ограничен в расширяемости и возможностях | 
Oauth2 и Keykloack
В нашем случае, для обеспечения функциональности сервера авторизации, мы используем Keycloak. Keycloak предоставляет не только механизм выдачи токенов, но и обширные возможности управления идентичностью, включая аутентификацию, авторизацию и управление пользователями. Таким образом, при использовании Keycloak с OAuth2 ваше веб-приложение может делегировать процесс аутентификации и управление доступом к Keycloak, который выступает в роли вашего собственного сервера авторизации. Это обеспечивает высокий уровень безопасности и управляемости в процессе работы с данными пользователей, при этом не требуя передачи конфиденциальных учетных данных вашему веб-приложению.
Установка и настройка Keycloak
Начнем с установки Keycloak. Вы можете скачать его с его официального сайта https://www.keycloak.org/downloads.html и следовать инструкциям по установке для вашей операционной системы. Для удобства установки и развертывания, мы будем использовать будем использовать Docker Compose файл и последнюю версию докер образа Keycloak, которая на данный момент является 16.1.1. В зависимости от выбранной версии Keycloak могут отличаться пользовательский интерфейс и некоторые другие элементы.
Создайте файл docker-compose.yml в вашем проекте и добавьте следующий пример контента файла:
version: '3'
services:
  postgres:
    image: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: keycloakdb
      POSTGRES_USER: keycloakuser
      POSTGRES_PASSWORD: keycloakpass
    networks:
      - keycloak-network
  keycloak:
    image: jboss/keycloak:16.1.1
    ports:
      - 8180:8080
    environment:
      DB_VENDOR: POSTGRES
      DB_ADDR: postgres
      DB_DATABASE: keycloakdb
      DB_SCHEMA: public
      DB_USER: keycloakuser
      DB_PASSWORD: keycloakpass
      KEYCLOAK_USER: admin
      KEYCLOAK_PASSWORD: adminpass
    networks:
      - keycloak-network
    depends_on:
      - postgres
networks:
  keycloak-network:
    driver: bridge
volumes:
  postgres_data:
    driver: localKeycloak по умолчанию использует встроенную базу данных H2. Однако для production развертывания рекомендуется использовать более надежную и масштабируемую базу данных, такую как PostgreSQL, MySQL или MariaDB. В предоставленном файле Docker Compose конфигурация настроена на использование PostgreSQL в качестве базы данных для Keycloak.
Запустите команду docker-compose up из директории проекта, где находится файл docker-compose.yml. После выполнения этой команды, сервер Keycloak начнет работу на порту 8180. Для доступа к административной консоли перейдите по следующей ссылке: http://localhost:8180/.
После этого введите администраторский логин и пароль, указанные в файле docker-compose.yml (admin, adminpass).
Создаем Realm:
"Realm" в Keycloak - это административная единица (контейнер), которая объединяет набор клиентов (applications), пользователей и настроек безопасности в рамках одной области. Каждое приложение в Keycloak принадлежит определенному realm, и realm предоставляет изолированное пространство для управления пользователями, аутентификации и авторизации.
После успешного входа, перейдите к следующему шагу и создайте новый realm, нажав "Add realm".

Укажите имя и нажмите "Create".

Клиент представляет собой ваше веб-приложение, которое будет взаимодействовать с Keycloak для аутентификации.
Перейдите к следующему шагу и внутри вашего Realm создайте клиента. В левом боковом меню перейдите в раздел "Clients" и нажмите "Create" в правом верхнем углу.
 Создайте клиента, указав Client ID, и нажмите "Save".

Перейдём к настройке клиента.
Установите тип доступа (Access Type) на "confidential", отключите "Direct Access Grants Enabled", включите "Service Accounts Enabled", укажите допустимые перенаправления (Valid redirect URIs) как http://localhost:8080/* и нажмите "Save".


Перейдите к "Client Scopes" -> "Roles" -> "Mappers" -> "Realm Roles" и включите "Add to userinfo", затем нажмите "Save".

Добавим роли для клиента:
1. Перейдите в раздел "Clients" и выберите конкретного клиента (например, "myclient").
2. Внутри клиента перейдите в "Service Account Roles" -> "Client Roles".
3. Нажмите на "Select a Client" и выберите "realm-management" из списка
4. В разделе "Available Roles" найдите роли "manage-users", "query-users", "view-users", "view-realm". Нажмите на каждую из этих ролей, чтобы выделить их. Нажмите "Add Selected", чтобы добавить выбранные роли к служебной учетной записи вашего клиента.
Эти настройки гарантируют, что служебная учетная запись вашего клиента будет иметь соответствующие права доступа к управлению пользователями, запросу данных, просмотру пользователей.

С помощью импорта разработчики могут избежать необходимости повторного ввода одних и тех же настроек безопасности. Это снижает риск возникновения ошибок из-за человеческого фактора и ускоряет процесс разработки.
 Для экспорта реалма необходимо перейти в раздел "Export" в левом боковом меню, включить опции "Export groups and roles" и "Export clients", затем нажать кнопку "Export". 

Экспортированный из пользовательского интерфейса Keycloak файл не включает информацию о пользователях и секретах клиента. Однако существует метод добавления секрета в файл экспорта realm в формате JSON. Откройте его для редактирования.
Для добавления секрета клиента найдите клиента с секретом, который нужно сохранить (например, поиск по "clientId": "myclient"), затем найдите "secret": "**********" внутри него и замените звездочки на секрет клиента, который нужно сохранить.
После этого добавьте экспортированный файл в приложение и добавьте следующие изменения в docker-compose.yml файле
keycloak:
      KEYCLOAK_IMPORT: /opt/jboss/keycloak/standalone/configuration/realm-export.json
    volumes:
      - ./realm-export.json:/opt/jboss/keycloak/standalone/configuration/realm-export.jsonЗапустите команду docker-compose up из директории проекта, где находится файл docker-compose.yml.
Как создать пользователя:
Перейдите в раздел "Users".
Нажмите на "Add user", укажите желаемое имя пользователя (например, testuser) и нажмите "Save".

Перейдите в "Users" -> "testuser" -> "Credentials".
Укажите пароль, повторите пароль и нажмите "Set Password".
Опционально, можно отключить опцию "Temporary", чтобы при первом входе пользователь был обязан изменить пароль.

Как создать роли:
Перейдите в раздел "Roles".
Нажмите на "Add role", укажите название роли (например, admin) и нажмите "Save".

Как назначить роль пользователю:
Перейдите в раздел "Users".
Выберите нужного пользователя, например, "testuser".
Перейдите в "Role Mappings" -> "Realm roles" -> "Available".
Нажмите на нужную роль, например, "admin", и затем "Add Selected".

Группы. Как их создать
1. Организация Пользователей:
В Keycloak группы используются для организации пользователей. Вы можете создавать группы и добавлять пользователей. Пользователи наследуют атрибуты и отображения ролей, присвоенные каждой группе. Группы иерархичны. Группа может иметь много подгрупп, но у группы может быть только один родитель. Подгруппы наследуют атрибуты и отображения ролей от родительской группы. Таким образом, если у вас есть родительская группа и дочерняя группа, и пользователь принадлежит только к дочерней группе, то пользователь наследует атрибуты и отображения ролей как от родительской, так и от дочерней группы.
2. Управление Доступом:
Группы предоставляют удобный способ управления правами доступа. Например, если у вас есть приложение с разными разделами или функциональностью, вы можете создать группы, представляющие эти разделы, и назначить пользователям соответствующие группы для управления их доступом.
3. Роли в Группах:
В Keycloak группы могут также содержать роли, которые определяют права доступа. Например, у вас может быть группа "Администраторы" с ролью "Администратор", что позволяет пользователям в этой группе иметь особые привилегии.
Для создания групп перейдите на вкладку "Groups", затем нажмите "New", укажите название группы и нажмите "Save".

Для добавления роли к группе перейдите на вкладку Role Mappings внутри группы, из Available roles выберите нужную роль, например, "admin", и затем "Add Selected".

Для добавления пользователя в группу перейдите на вкладку "Users", выберите нужного пользователя, затем перейдите на вкладку "Groups". Внутри профиля пользователя в разделе "Available groups" выберите нужную группу и нажмите "Join".

Как добавить дополнительные атрибуты в Access токен:
Access token содержит некоторые метаданные, но мы можем добавить в него атрибуты. Давайте посмотрим, как добавить пользовательский атрибут в токен. Перейдите на вкладку Users, выберите пользователя, затем откройте вкладку Attributes. Нажмите кнопку Add, добавьте ключ для атрибута (например, customAttribute) и значение (например, customAttributeValue), затем нажмите Save.

После этого нам нужно добавить маппер для этого атрибута. Чтобы сделать это, перейдите в раздел Clients, выберите свой клиент, затем перейдите на вкладку Mappers и нажмите Create. Укажите имя маппера (например, customMapper), установите тип маппера как User Attribute, выберите Claim JSON Type как string (вы можете выбрать другие типы в зависимости от ваших потребностей), укажите имя атрибута пользователя и имя атрибута токена как customAttribute, затем нажмите Save.

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

В этой части статьи мы установили и настроили Keycloak, подключили базу данных для использования Keycloak, создали Realm, создали и настроили клиента, экспортировали и импортировали Realm, создали пользователей, создали роли, назначили роли пользователям, создали группу, добавили роль к группе, добавили пользователей в группу и добавили дополнительный атрибут в Access token. Если вы хотите использовать другую конфигурацию, добавить дополнительные настройки или выполнить другие действия, вы можете найти соответствующую информацию на официальной странице документации Keycloak по следующей ссылке https://www.keycloak.org/documentation.

Настройка безопасности приложения Spring MVC
Теперь перейдем к конфигурации нашего приложения Spring Boot и добавим безопасность.
Добавьте следующие зависимости:
 dependencies {
    // Другие зависимости вашего проекта
    // Зависимость для поддержки OAuth2-клиента в Spring Boot
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    // Зависимость для поддержки безопасности в Spring Boot
    implementation 'org.springframework.boot:spring-boot-starter-security'
}Теперь добавим конфигурационный класс для безопасности (security).
Тут большая страшилка
import static org.springframework.security.config.Customizer.withDefaults;
import java.net.URI;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true
)
public class SecurityConfig {
  
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler) throws Exception {
        return http.authorizeHttpRequests(authorise ->
                        authorise
                                .requestMatchers("/static/**")
                                .permitAll()
                                .anyRequest()
                                .authenticated())
                .csrf(AbstractHttpConfigurer::disable)
                .exceptionHandling(authorise -> authorise.accessDeniedPage("/access-denied"))
                .oauth2Login(withDefaults())
                .logout(logout ->
                        logout.logoutSuccessHandler(oidcLogoutSuccessHandler)).build();
}
    @Bean
    @SuppressWarnings("unchecked")
    public GrantedAuthoritiesMapper userAuthoritiesMapper() {
        return (authorities) -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
            authorities.forEach(authority -> {
                if (authority instanceof OidcUserAuthority oidcUserAuthority) {
                    OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
                    Map<String, Object> realmAccess = userInfo.getClaim("realm_access");
                    Collection<String> realmRoles;
                    if (realmAccess != null
                            && (realmRoles = (Collection<String>) realmAccess.get("roles")) != null) {
                        realmRoles
                                .forEach(role -> mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
                    }
                }
            });
            return mappedAuthorities;
        };
    }
    @Bean
    OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler(ClientRegistrationRepository clientRegistrationRepository) {
        OidcClientInitiatedLogoutSuccessHandler successHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
        successHandler.setPostLogoutRedirectUri(URI.create("http://localhost:8080").toString());
        return successHandler;
    }
    @Bean
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }
} В файле application.yml добавим следующие настройки
spring:
  security:
    oauth2:
      client:
        registration:
          keycloak:
            client-id: myrealm
            client-secret: chnageme
            scope: openid, profile
            authorization-grant-type: authorization_code
        provider:
          keycloak:
            issuer-uri: http://localhost:8180/auth/realms/myrealm
            user-name-attribute: preferred_username
keycloak:
  server-url: http://localhost:8180/auth
  realm: myrealm
  username: admin
  password: adminpassЗамените значение changeme на уникальный секрет, который был сгенерирован для вашего клиента в Keycloak. Чтобы сгенерировать новый client-secret, перейдите в меню 'Clients'. В разделе 'Credentials' скопируйте существующий client-secret или, если его нет, нажмите 'Regenerate Secret', чтобы получить новое значение. Затем вставьте его в качестве значения для параметра client-secret в файле конфигурации вашего приложения (application.yml).
Отметим, что данная конфигурация предназначена для архитектуры MVC, когда наше приложение выступает в роли клиента и ресурсного сервера. Если ваше приложение является REST приложением и предназначено только для роли ресурсного сервера, вы можете найти соответствующую конфигурацию по следующим ссылкам:
https://github.com/Pask423/keycloak-springboot/tree/master/base-integration-spring-boot-3
https://www.youtube.com/watch?v=vmEWywGzWbA
Программное взаимодействие с Keycloak через Admin Client
Admin Client в Keycloak позволяет программно создавать пользователей и проводить разнообразные операции, обеспечивая автоматизацию административных задач без необходимости использования веб-консоли. Это удобство позволяет эффективно управлять идентификацией и доступом в системе.
Для использования админ клиента необходимо добавить следующий зависимость:
'org.keycloak:keycloak-admin-client:22.0.1'
Создай конфигурационый класс KeyCloakConfig, который вернет экземпляр Keycloak.
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class KeycloakConfig {
  
    @Value("${keycloak.server-url}")
    private String serverUrl;
    @Value("${keycloak.realm}")
    private String realm;
    @Value("${spring.security.oauth2.client.registration.keycloak.client-id}")
    private String clientId;
    @Value("${spring.security.oauth2.client.registration.keycloak.client-secret}")
    private String clientSecret;
    @Value("${keycloak.username}")
    private String userName;
    @Value("${keycloak.password}")
    private String password;
    @Bean
    public Keycloak keycloak() {
      
        return KeycloakBuilder.builder()
                .serverUrl(serverUrl)
                .realm(realm)
                .grantType(OAuth2Constants.CLIENT_CREDENTIALS)
                .clientId(clientId)
                .clientSecret(clientSecret)
                .username(userName)
                .password(password)
                .build();
    }
}Тестирование функционала
Для тестирования всего функционала создаем:
Простой Controller, DTO и Service
import jakarta.annotation.security.RolesAllowed;
import java.security.Principal;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@Controller
@RequiredArgsConstructor
public class MyController {
  
    private final KeyCloakService keyCloakService;
    @GetMapping("/admin")
    @RolesAllowed("admin")
    public String admin(Principal principal, Model model) {
        model.addAttribute("username", principal.getName());
        return "admin";
    }
    @GetMapping("/user")
    public String user(Principal principal, Model model) {
        model.addAttribute("username", principal.getName());
        return "user";
    }
    @GetMapping("/create")
    public String createUser() {
        return "create-user";
    }
    @PostMapping("/create")
    public String createUser(@RequestBody UserRequestDTO userRequestDTO) {
        keyCloakService.addUser(userRequestDTO);
        return "index";
    }
}
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class ErrorController {
    @GetMapping("/access-denied")
    public ModelAndView showAccessDeniedPage() {
        return new ModelAndView("access-denied");
    }
}
import lombok.Data;
@Data
public class UserRequestDTO {
    private String username;
    private String password;
    private String role;
}
 
import java.util.Collections;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RoleMappingResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;  
@Service
@RequiredArgsConstructor
public class KeyCloakService {
    private final Keycloak keycloak;
    @Value("${keycloak.realm}")
    private String realm;
    public void addUser(UserRequestDTO dto) {
        String username = dto.getUsername();
        CredentialRepresentation credential = createPasswordCredentials(dto.getPassword());
        UserRepresentation user = new UserRepresentation();
        user.setUsername(username);
        user.setCredentials(Collections.singletonList(credential));
        user.setEnabled(true);
        UsersResource usersResource = getUsersResource();
        usersResource.create(user);
        addRealmRoleToUser(username, dto.getRole());
    }
    private void addRealmRoleToUser(String userName, String roleName) {
        RealmResource realmResource = keycloak.realm(realm);
        List<UserRepresentation> users = realmResource.users().search(userName);
        UserResource userResource = realmResource.users().get(users.get(0).getId());
        RoleRepresentation role = realmResource.roles().get(roleName).toRepresentation();
        RoleMappingResource roleMappingResource = userResource.roles();
        roleMappingResource.realmLevel().add(Collections.singletonList(role));
    }
    private UsersResource getUsersResource() {
        return keycloak.realm(realm).users();
    }
    private static CredentialRepresentation createPasswordCredentials(String password) {
        CredentialRepresentation passwordCredentials = new CredentialRepresentation();
        passwordCredentials.setTemporary(false);
        passwordCredentials.setType(CredentialRepresentation.PASSWORD);
        passwordCredentials.setValue(password);
        return passwordCredentials;
    }
}Html страницы
index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Keycloak Spring Boot Integration</title>
</head>
<body>
<h1>Welcome to Keycloak Integration App</h1>
<p><a th:href="@{/admin}">Admin Page</a></p>
<p><a th:href="@{/user}">User Page</a></p>
<p><a th:href="@{/create}">Create User</a></p>
<p><a th:href="@{/logout}">Logout</a></p>
</body>
</html>access-denied.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Нет доступа</title>
</head>
<body>
<h1>You do not have permission to access this resource</h1>
<a href="index.html" th:href="@{~/}">
    <button>Return to home page</button>
</a>
</body>
</html>admin.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Admin Page</title>
</head>
<body>
<h1>Welcome, <span th:text="${username}"></span>!</h1>
<p>This is the Admin Page.</p>
<p><a th:href="@{/}">Return to Home Page</a></p>
<p><a th:href="@{/logout}">Logout</a></p>
</body>
</html>user.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>User Page</title>
</head>
<body>
<h1>Welcome, <span th:text="${username}"></span>!</h1>
<p>This is the User Page.</p>
<p><a th:href="@{/}">Return to Home Page</a></p>
<p><a th:href="@{/logout}">Logout</a></p>
</body>
</html>create-user.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Create user</title>
</head>
<body>
<form id="userForm" th:action="@{/create}" method="post">
    <label for="username">Username:</label>
    <input type="text" id="username" name="username" required>
    <br>
    <label for="password">Password:</label>
    <input type="password" id="password" name="password" required>
    <br>
    <label for="role">Role:</label>
    <input type="text" id="role" name="role" required>
    <br>
    <button type="button" onclick="submitForm()">Create User</button>
</form>
<script>
    function submitForm() {
        var form = document.getElementById('userForm');
        var formData = {
            username: form.elements.username.value,
            password: form.elements.password.value,
            role: form.elements.role.value
        };
        var jsonData = JSON.stringify(formData);
        fetch(form.action, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: jsonData
        });
    }
</script>
</body>
</html>Создаем двух пользователей в Keycloak с разными ролями, например, "firstuser" с ролью "admin" и "seconduser" с ролью "user".
В браузере перейдите по адресу http://localhost:8180/. Вас перенаправит на страницу аутентификации Keycloak. Войдите в систему как пользователь "firstuser".

Будет открываться главная страница


Нажмите на Admin Page и доступ будет запрещен, так как пользователь "seconduser" не имеет роль "admin"

Теперь программатично создаем пользователя с ролю. Нажмите на Create User и будет открываться страница для создания пользователя. Заполните поля 'username', 'password' и 'role' (выбрав одну из ролей, доступных в Keycloak), затем нажмите 'Create user'.

Перейдите в админ-консоль Keycloak по адресу localhost:8180, затем в меню 'Users' нажмите 'View All Users', и вы увидите созданного пользователя (testuser) с ролью 'admin'.



Мы внедрили безопасность в наше приложение, проверили и убедились, что ограничения по ролям работают, еще мы можем программно взаимодействовать с Keycloak, например, создавать пользователей.
Заключение
Этот проект можно посмотреть на GitHub по следующей ссылке.
Несмотря на то, что Keycloak является мощным инструментом для управления идентификацией и доступом, у него есть некоторые недостатки:
1. Сложность настройки
Некоторые пользователи могут столкнуться с трудностями при настройке Keycloak из-за обширного набора функций и параметров, что может потребовать времени и усилий.
2. Сложность обновлений
Переход на новые версии Keycloak иногда может быть сложным процессом из-за изменений в API и структуре данных.
В заключении отметим, что мы успешно произвели интеграцию Keycloak с протоколом OAuth 2 в приложении Spring Boot 3. Таким образом, наш опыт успешной интеграции Keycloak и OAuth 2 в Spring Boot 3 подтверждает, что данное решение не только соответствует высоким стандартам безопасности, но также обеспечивает удобство в разработке и поддержке нашего веб-приложения.
Мы можем подчеркнуть несколько важных моментов, касающихся интеграции Keycloak с протоколом OAuth 2 в приложении Spring Boot 3.
Во-первых, стоит отметить, что протокол OAuth 2 доказал свою эффективность в обеспечении безопасности и авторизации. Сравнив его с другими протоколами, мы выявили его гибкость и простоту в реализации, что делает его привлекательным выбором для многих сценариев авторизации.
В контексте интеграции с Keycloak, использование Docker Compose для настройки среды и PostgreSQL в качестве базы данных приносит дополнительные выгоды. Это позволяет эффективно управлять конфигурацией Keycloak, а также обеспечивает легкость масштабирования и переносимость между средами.
Внедрение импорта для Realm Keycloak существенно повышает уровень автоматизации настройки, что в свою очередь значительно упрощает и ускоряет процессы внедрения.
Стоит особо выделить важность интеграции административного клиента Keycloak. Этот элемент играет решающую роль в программном управлении процессами, предоставляя возможность их реализации через код. Эта интеграция не только предоставляет более гибкий и мощный способ управления Keycloak, но также существенно снижает необходимость вручную взаимодействовать с веб-консолью.
В итоге, интеграция Keycloak с протоколом OAuth 2, настроенного с использованием Docker Compose, в сочетании с программной интеграцией административного клиента в приложение Spring Boot 3, обеспечивает не только высокий уровень безопасности, но также максимальную степень автоматизации в управлении процессами аутентификации и авторизации.
Какие технологии вы используете для аутентификации и авторизации в своем Spring Boot приложении?
Комментарии (9)
 - GrigorTumanyan18.12.2023 08:32- Спасибо за статью, было очень интересно, рекомендую к прочтению. Я потратил много времени на чтение, но оно того стоило. 
 - Dmitry201918.12.2023 08:32- последнюю версию докер образа Keycloak, которая на данный момент является 16.1.1 - Последняя версия на сайте 23.0.3  - HakobDiloyan Автор18.12.2023 08:32- Да, здесь есть упущение. Последняя версия докер образа Keycloak от Jboss является 16.1.1, который я использовал, а последняя версия quay.io/keycloak 23.0.3. Спасибо, что заметили.  - DonAlPAtino18.12.2023 08:32- А какие причины использование старой версии? У меня вот user не создается с 400 Bad request. Пишут из-за несовпадения версий сервера и клиента. А тут такой немаленький gap выходит... 
 
 
 - aamatveef18.12.2023 08:32- Отличная статья, было бы интересно в таком же формате почитать про Authorization в связке со Spring boot 
 - DonAlPAtino18.12.2023 08:32- > В браузере перейдите по адресу http://localhost:8180/. Вас перенаправит на страницу аутентификации Keycloak - 8080 же. Мы же на спринговое приложение идем, а уже оно редиректит на keycloak  - HakobDiloyan Автор18.12.2023 08:32- http://localhost:8180 - в этом случае ссылка на админ-консоль Keycloak. Для аутентификации должен быть пользователь, зарегистрированный в Keycloak, чтобы могли войти в систему через данные этого пользователя. Или можно создать регистрация пользователей, а затем использовать данные зарегистрированного пользователя для входа. 
 
 
           
 
remindscope
Отличная статья, спасибо