Использование Keycloak с Spring Boot

В этом руководстве мы обсудим основы настройки сервера Keycloak и подключения к нему приложения Spring Boot с использованием Spring Security OAuth2.0.

1. Что такое Keycloak?

Keycloak — это решение для управления идентификацией и доступом с открытым исходным кодом, ориентированное на современные приложения и услуги.

Keycloak предлагает такие функции, как единый вход (SSO), посредничество идентификаций и вход через социальные сети, федерацию пользователей, клиентские адаптеры, консоль администратора и консоль управления учетными записями.

В нашем руководстве мы будем использовать консоль администратора Keycloak для настройки и подключения к Spring Boot с использованием Spring Security OAuth2.0.

2. Настройка сервера Keycloak

В этом разделе мы настроим и настроим сервер Keycloak.

2.1. Загрузка и установка Keycloak

Есть несколько дистрибутивов на выбор. Однако в этом уроке мы будем использовать автономную версию.

Давайте скачаем дистрибутив автономного сервера Keycloak-22.0.3 из официального источника.

Загрузив дистрибутив автономного сервера, мы можем разархивировать и запустить Keycloak с терминала:

unzip keycloak-22.0.3.zip 
cd keycloak-22.0.3
bin/kc.sh start-dev

После выполнения этих команд Keycloak запустит свои службы. Как только мы увидим строку, содержащую Keycloak 22.0.3 […] start , мы будем знать, что его запуск завершен.

Теперь давайте откроем браузер и посетим http://localhost:8080. Мы будем перенаправлены на http://localhost:8080/auth для создания административного входа:

 

плащ-ключДавайте создадим начального пользователя-администратора с именем Initial1 и паролем zaq1!QAZ . Нажав «Создать» , мы увидим сообщение «Создано пользователем».

Теперь мы можем перейти к административной консоли. На странице входа мы введем первоначальные учетные данные администратора:

консоль администратора keycloak

2.2. Создание Realm

Успешный вход в систему приведет нас к консоли и откроет для нас мастер- realm по умолчанию.

Здесь мы сосредоточимся на создании пользовательского realm.


Давайте перейдем в верхний левый угол, чтобы найти кнопку «Создать область» :

создать царство

На следующем экране давайте добавим новую область под названием SpringBootKeycloak :

создать имя областиПосле нажатия кнопки «Создать» будет создан новый realm, и мы будем перенаправлены в него. Все операции в следующих разделах будут выполняться в этой новой области SpringBootKeycloak.

2.3. Создание клиента

Теперь мы перейдем на страницу «Клиенты». Как мы видим на изображении ниже, Keycloak поставляется с уже встроенными клиентами:

клиенты keycloakНам все еще нужно добавить в наше приложение нового клиента, поэтому нажмем «Создать» . Мы назовем новое приложение для входа в систему клиента:

создать клиентаНа следующем экране для целей данного руководства мы оставим все значения по умолчанию, кроме поля «Действительные URI перенаправления» . Это поле должно содержать URL-адреса приложений, которые будут использовать этот клиент для аутентификации :

keycloak перенаправляет URIПозже мы создадим приложение Spring Boot, работающее на порту 8081, которое будет использовать этот клиент. Поэтому мы использовали URL-адрес перенаправления http://localhost:8081/ * выше.

2.4. Создание роли и пользователя

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


Для этого нам нужно перейти на страницу «Роли области» :

роли в миреЗатем мы добавим роль пользователя :

создать роль

Теперь у нас есть роль, которую можно назначить пользователям, но поскольку пользователей пока нет, давайте перейдем на страницу «Пользователи» и добавим одного:

Создать пользователяМы добавим пользователя с именем user1:

Создать пользователяПосле создания пользователя откроется страница с его данными:

данные пользователяТеперь мы можем перейти на вкладку «Учетные данные» . Мы установим первоначальный пароль xsw2@WS:

пароль пользователяНаконец, мы перейдем на вкладку «Сопоставление ролей» . Мы назначим роль пользователя нашему пользователю user1:

назначить рольНаконец, нам нужно правильно настроить области клиента, чтобы KeyCloak передавал все роли аутентифицирующего пользователя токену. Поэтому нам нужно перейти на страницу «Области клиента» и затем установить для microprofile-jwt значение «default» , как показано на рисунке ниже.

Область действия клиента KeyCloak3. Генерация токенов доступа с помощью API Keycloak

Keycloak предоставляет REST API для создания и обновления токенов доступа. Мы можем легко использовать этот API для создания собственной страницы входа.

Во-первых, нам нужно получить токен доступа от Keycloak, отправив POST-запрос на этот URL-адрес:

http://localhost:8080/realms/SpringBootKeycloak/protocol/openid-connect/token

Тело запроса должно иметь формат x-www-form-urlencoded :

client_id:<your_client_id>
username:<your_username>
password:<your_password>
grant_type:password

В ответ мы получим access_token и refresh_token.

Токен доступа следует использовать в каждом запросе к ресурсу, защищенному Keycloak, просто помещая его в заголовок авторизации :

headers: {
    'Authorization': 'Bearer' + access_token
}

По истечении срока действия токена доступа мы можем обновить его, отправив запрос POST на тот же URL-адрес, что и выше, но содержащий токен обновления вместо имени пользователя и пароля:

{
    'client_id': 'your_client_id',
    'refresh_token': refresh_token_from_previous_request,
    'grant_type': 'refresh_token'
}

Keycloak ответит на это новыми access_token и refresh_token.

4. Создание и настройка приложения Spring Boot

В этом разделе мы создадим приложение Spring Boot и настроим его как клиент OAuth для взаимодействия с сервером Keycloak.

4.1. Зависимости

Мы используем клиент Spring Security OAuth2.0 для подключения к серверу Keycloak.

Начнем с объявления зависимости Spring-boot-starter-oauth2-client в приложении Spring Boot в pom.xml :

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

Кроме того, поскольку нам нужно использовать Spring Security с Spring Boot, мы должны добавить эту зависимость :

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

Чтобы делегировать контроль идентификации серверу Keycloak, мы будем использовать библиотеку Spring-boot-starter-oauth2-resource-server. Это позволит нам проверить токен JWT на сервере Keycloak. Следовательно, давайте добавим его в наш pom:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

Теперь приложение Spring Boot может взаимодействовать с Keycloak.

4.2. Конфигурация Keycloak

Мы рассматриваем клиент Keycloak как клиент OAuth. Итак, нам нужно настроить приложение Spring Boot для использования OAuth Client.

Класс ClientRegistration содержит всю основную информацию о клиенте. Автоконфигурация Spring ищет свойства со схемой Spring.security.oauth2.client.registration.[registrationId] и регистрирует клиента с помощью OAuth 2.0 или OpenID Connect (OIDC) .

Настроим конфигурацию регистрации клиента:

spring.security.oauth2.client.registration.keycloak.client-id=login-app
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.scope=openid

Значение, которое мы указываем в client-id,  соответствует клиенту, который мы назвали в консоли администратора.

Приложению Spring Boot необходимо взаимодействовать с поставщиком OAuth 2.0 или OIDC для обработки фактической логики запроса для различных типов грантов. Итак, нам нужно настроить провайдера OIDC. Его можно автоматически настроить на основе значений свойств с помощью схемы Spring.security.oauth2.client.provider.[имя поставщика].

Давайте настроим конфигурацию провайдера OIDC:

spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/realms/SpringBootKeycloak
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username

Как мы помним, мы запустили Keycloak на порту 8080 , следовательно, путь указан в Issuer-uri . Это свойство идентифицирует базовый URI для сервера авторизации. Мы вводим имя области, которую мы создали, в консоли администратора Keycloak. Кроме того, мы можем определить атрибут имени пользователя как предпочтительное_имя_пользователя , чтобы заполнить Принципал нашего контроллера подходящим пользователем.

Наконец, давайте добавим конфигурацию, необходимую для проверки токена JWT на нашем сервере Keycloak:

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/SpringBootKeycloak

4.3. Класс конфигурации

Мы настраиваем HttpSecurity , создавая bean-компонент SecurityFilterChain . Кроме того, нам нужно включить вход в систему OAuth2 с помощью http.oauth2Login().

Давайте создадим конфигурацию безопасности:

@Configuration
@EnableWebSecurity
class SecurityConfig {

    private static final String GROUPS = "groups";
    private static final String REALM_ACCESS_CLAIM = "realm_access";
    private static final String ROLES_CLAIM = "roles";

    private final KeycloakLogoutHandler keycloakLogoutHandler;

    SecurityConfig(KeycloakLogoutHandler keycloakLogoutHandler) {
        this.keycloakLogoutHandler = keycloakLogoutHandler;
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Bean
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(sessionRegistry());
    }

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }
    
    @Bean
    public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
            .requestMatchers(new AntPathRequestMatcher("/customers*"))
            .hasRole("user")
            .requestMatchers(new AntPathRequestMatcher("/"))
            .permitAll()
            .anyRequest()
            .authenticated());
        http.oauth2ResourceServer((oauth2) -> oauth2
            .jwt(Customizer.withDefaults()));
        http.oauth2Login(Customizer.withDefaults())
            .logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/"));
        return http.build();
    }
    
    @Bean
    public GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() {
        return authorities -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
            var authority = authorities.iterator().next();
            boolean isOidc = authority instanceof OidcUserAuthority;

            if (isOidc) {
                var oidcUserAuthority = (OidcUserAuthority) authority;
                var userInfo = oidcUserAuthority.getUserInfo();

                // Tokens can be configured to return roles under
                // Groups or REALM ACCESS hence have to check both
                if (userInfo.hasClaim(REALM_ACCESS_CLAIM)) {
                    var realmAccess = userInfo.getClaimAsMap(REALM_ACCESS_CLAIM);
                    var roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
                    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
                } else if (userInfo.hasClaim(GROUPS)) {
                    Collection<String> roles = (Collection<String>) userInfo.getClaim(
                        GROUPS);
                    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
                }
            } else {
                var oauth2UserAuthority = (OAuth2UserAuthority) authority;
                Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();

                if (userAttributes.containsKey(REALM_ACCESS_CLAIM)) {
                    Map<String, Object> realmAccess = (Map<String, Object>) userAttributes.get(
                        REALM_ACCESS_CLAIM);
                    Collection<String> roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
                    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
                }
            }
            return mappedAuthorities;
        };
    }

    Collection<GrantedAuthority> generateAuthoritiesFromClaim(Collection<String> roles) {
        return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(
            Collectors.toList());
    }
}

В приведенном выше коде метод oauth2Login() добавляет OAuth2LoginAuthenticationFilter в цепочку фильтров. Этот фильтр перехватывает запросы и применяет необходимую логику для аутентификации OAuth 2. Метод oauth2ResourceServer проверит привязанный токен JWT на нашем сервере Keycloak.


Мы настраиваем доступ на основе полномочий и ролей в методе configure() . Эти ограничения гарантируют, что каждый запрос к /customers/* будет авторизован только в том случае, если запрашивающий его является аутентифицированным пользователем с ролью USER.

Наконец, нам нужно обработать выход из Keycloak. Для этого добавим класс KeycloakLogoutHandler:

@Component
public class KeycloakLogoutHandler implements LogoutHandler {

    private static final Logger logger = LoggerFactory.getLogger(KeycloakLogoutHandler.class);
    private final RestTemplate restTemplate;

    public KeycloakLogoutHandler(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, 
      Authentication auth) {
        logoutFromKeycloak((OidcUser) auth.getPrincipal());
    }

    private void logoutFromKeycloak(OidcUser user) {
        String endSessionEndpoint = user.getIssuer() + "/protocol/openid-connect/logout";
        UriComponentsBuilder builder = UriComponentsBuilder
          .fromUriString(endSessionEndpoint)
          .queryParam("id_token_hint", user.getIdToken().getTokenValue());

        ResponseEntity<String> logoutResponse = restTemplate.getForEntity(
        builder.toUriString(), String.class);
        if (logoutResponse.getStatusCode().is2xxSuccessful()) {
            logger.info("Successfulley logged out from Keycloak");
        } else {
            logger.error("Could not propagate logout to Keycloak");
        }
    }

}

Класс KeycloakLogoutHandler реализует класс LogoutHandler и отправляет запрос на выход из системы Keycloak.

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

4.4. Веб-страницы Thymeleaf

Мы используем Thymeleaf для наших веб-страниц.

У нас есть три страницы:

  • external.html – внешняя веб-страница для общественности.
  • customer.html — внутренняя страница, доступ к которой будет ограничен только аутентифицированными пользователями с ролью user .
  • Layout.html – простой макет, состоящий из двух фрагментов, которые используются как для внешней, так и для внутренней страницы.

Код шаблонов Thymeleaf доступен на Github .

4.5. Контроллер

Веб-контроллер сопоставляет внутренние и внешние URL-адреса с соответствующими шаблонами Thymeleaf:

@GetMapping(path = "/")
public String index() {
    return "external";
}
    
@GetMapping(path = "/customers")
public String customers(Principal principal, Model model) {
    addCustomers();
    model.addAttribute("customers", customerDAO.findAll());
    model.addAttribute("username", principal.getName());
    return "customers";
}

Для пути /customers мы извлекаем всех клиентов из репозитория и добавляем результат в качестве атрибута в Model . Позже мы перебираем результаты в Thymeleaf.

Чтобы иметь возможность отображать имя пользователя, мы также добавляем Principal.

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

5. Демонстрация

Теперь мы готовы протестировать наше приложение. Чтобы запустить приложение Spring Boot, мы можем легко запустить его через IDE, например Spring Tool Suite (STS), или запустить эту команду в терминале:

mvn clean spring-boot:run

При посещении http://localhost:8081 мы видим:

Внешняя страница KeycloakТеперь мы нажимаем «клиенты» , чтобы войти в интранет, где хранится конфиденциальная информация.

Обратите внимание, что мы были перенаправлены для аутентификации через Keycloak, чтобы узнать, авторизованы ли мы для просмотра этого контента:

вход в систему keycloak

Как только мы войдем в систему как user1 , Keycloak проверит нашу авторизацию на то, что у нас есть роль пользователя , и мы будем перенаправлены на страницу клиентов с ограниченным доступом :

страница клиентовТеперь мы закончили настройку подключения Spring Boot к Keycloak и демонстрируем, как это работает.

Как мы видим, Spring Boot беспрепятственно обработал весь процесс вызова сервера авторизации Keycloak . Нам не пришлось самостоятельно вызывать API Keycloak для генерации токена доступа или даже явно отправлять заголовок авторизации в нашем запросе на защищенные ресурсы.

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

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

Мы также узнали, как настроить Spring Security и использовать его вместе с Keycloak.