Keycloak интеграция со Spring boot

Введение

Данная стать является инструкцией для новичков, которые хотели бы использовать Keycloak в своих проектах в качестве безопасности. В статье будет рассказано:

  1. Что такое Keycloak и для чего он нужен.

  2. Как запустить Keycloak.

  3. Как создать свой первый realm.

  4. Как настроить Keycloak.

  5. Как интегрировать Keycloak в свое приложение на Spring.

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

Keycloak

Для начала разберемся для чего нужен keycloak.

Keycloak — это система управления доступом с открытым исходным кодом, которая позволяет добавлять аутентификацию и авторизацию в приложения и сервисы. Она предоставляет функции единого входа (SSO), а также поддерживает различные протоколы аутентификации, такие как OpenID Connect, OAuth 2.0 и SAML.

Основные возможности Keycloak:

  1. Единый вход (SSO): позволяет пользователям входить в систему один раз и получать доступ ко всем связанным приложениям без повторной аутентификации.

  2. Социальная аутентификация: поддержка интеграции с социальными сетями, такими как Google, Facebook, Twitter, для аутентификации пользователей.

  3. Централизованное управление пользователями: администраторы могут управлять пользователями, ролями и разрешениями из единого интерфейса.

  4. Поддержка различных протоколов: как упоминалось ранее, Keycloak поддерживает множество стандартных протоколов аутентификации и авторизации.

  5. Настраиваемая и расширяемая: возможности платформы можно расширить за счет использования различных плагинов и адаптеров.

  6. Интеграция с LDAP и Active Directory: возможность интеграции с существующими системами управления пользователями.

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

Запуск Keycloak

Для начала создадим наш проект Spring, для этого можно использовать открытый для всех spring initializer — https://start.spring.io/ или же, если есть IntelliJ IDEA Ultimate, то можно создать проект Spring прям в ней.

Добавим нужные зависимости и создадим проект:

Spring Initializer и нужные зависимости

Spring Initializer и нужные зависимости

После того, как наш проект сформируется, у нас появится файл pom.xml, в котором будут прописаны все наши зависимости:

pom.xml



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        3.3.5
         
    
    org.example
    OauthTest
    0.0.1-SNAPSHOT
    OauthTest
    OauthTest
    
    
        
    
    
        
    
    
        
        
        
        
    
    
        17
    
    
        
            org.springframework.boot
            spring-boot-starter-oauth2-client
        
        
            org.springframework.boot
            spring-boot-starter-oauth2-resource-server
        
        
            org.springframework.boot
            spring-boot-starter-security
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            org.springframework.security
            spring-security-test
            test
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            org.projectlombok
                            lombok
                        
                    
                
            
        
    

Для дальнейшего использования Keycloak нам нужно прописать еще одну зависимость. Для того чтобы найти нужную зависимость и актуальную версию, можно использовать сайт — https://mvnrepository.com/

 
        
            org.keycloak
            keycloak-admin-client
            26.0.2
        

Чтобы запустить keycloak, я буду использовать Docker. Для этого создадим в корневой папке нашего проекта файл docker-compose.yml и пропишем в нем загрузку образов и создание контейнеров. Также я буду использовать вместо базы данных H2, которая установлена по-умолчанию в keycloak, базу данных posgreSQL, которую также запустил в Docker и подключил к keycloak.

docker-compose.yml

services:
  postgres:
    container_name: Postgres
    image: postgres
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: root
      POSTGRES_DB: keycloak
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - 5435:5432
    networks:
      - keycloak-network
    restart: unless-stopped

  keycloak:
    container_name: Keycloak
    image: quay.io/keycloak/keycloak:latest
    ports:
      - 9090:8080
    environment:
      DB_VENDOR: POSTGRES
      DB_ADDR: postgres
      DB_DATABASE: keycloak
      DB_SCHEMA: public
      DB_USER: root
      DB_PASSWORD: root
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    networks:
      - keycloak-network
    depends_on:
      - postgres
    command:
      - "start-dev"
networks:
  keycloak-network:
    driver: bridge

volumes:
  postgres_data:
    driver: local
  keycloak:
    driver: local

Наш keycloak будет работать на порту 9090. После запуска контейнера мы можем перейти на наш сервер по ссылке http://localhost:9090. Для того, чтобы войти в настройки keycloak, нам нужно вести данные, которые мы указали при создании контейнера.

KEYCLOAK_ADMIN: admin

KEYCLOAK_ADMIN_PASSWORD: admin

Вход в Keycloak

Вход в Keycloak

Keycloak запущен, можно приступать к настройкам.

Настройка Keycloak

После входа, нам нужно создать наш realm. Realm (область) — это пространство, которое управляет набором пользователей, учетных данных и ролей. Реалмы позволяют изолировать данные и настройки, чтобы различные приложения и пользователи могли существовать в одном экземпляре Keycloak без пересечения данных.

Создание нового realm

Создание нового realm

Создание нового realm

Даём название нашему realm

Даём название нашему realm

После создания нашего realm нам нужно создать нового client. Для этого переходим в раздел Clients и нажимаем кнопку Create client.

Создание своего Client

Созданием нового client

Созданием нового client

  1. Даём имя нашему client.

Даем название client

Даем название client

  1. Устанавливаем в Client authentication флаг на On.

Устанавливаем флаг On

Устанавливаем флаг On

  1. Прописываем наши URL.

Прописываем наши URL

Прописываем наши URL

Данный URL будет использовать наш бэк. В дальнейшем мы это все пропишем.

После того как мы создали нашего client, создадим нового пользователя (User). Для этого перейдем во вкладку Users. Здесь нам нужно создать пользователя, указав логин и пароль.

Создание нового пользователя

Создание нового пользователя

Создание нового пользователя

Пропишем данные в полях.

Заполнение данных

Заполнение данных

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

Добавление пароля

Добавление пароля

Прописывание пароля

Прописывание пароля

Теперь у нас есть пользователь в базе данных. Давайте проверим можно ли войти под этими данными. Для этого создадим файл scratch.http и пропишем следующее:

POST http://localhost:9090/realms/OauthTests/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded

client_id=myclient&client_secret=SECRET&username=kudzip&password=test&grant_type=password

client_id — это название, созданного нами client.

client_secret — это секретный ключ нашего client, его можно скопировать следуя инструкциям в пункте Копирование client secret. Данный ключ нужно вставить вместо слова SECRET.

Копирование client secret
Поиск секретного ключа нашего client

Поиск секретного ключа нашего client

Копирование client secret

Копирование client secret

username и password — это username и password, созданного нами пользователя.

После отправления запроса нам в ответ генерируется access_token и refresh_token:

Отправление запроса

Отправление запроса

Данный запрос можно выполнить в postman.

Как видно из изображения, все работает исправно. Проверим данный токен на сайте — JWT.IO.

Проверка JWT

Проверка JWT

Данный токен несет в себе основную информацию о пользователе. В поле «полезная нагрузка» можно увидеть все нужные данные. (email, role, name, family, username и тд.)

Во вкладке Clients можно посмотреть какую информацию у нас будет хранить JWT. Давайте на это посмотрим. Для этого нужно перейти во вкладку Clients и нажать на созданный нами myclient.

Просмотр токена

Просмотр нашего Client

Просмотр нашего Client

Просмотр access token

Просмотр access token

В поле Users нужно выбрать интересующего нас пользователя.

{
  "exp": 1733502948,
  "iat": 1733502648,
  "jti": "85cd30db-8b48-49a0-bf91-45a8e649e20a",
  "iss": "http://localhost:9090/realms/OauthTests",
  "aud": "account",
  "sub": "4b65107a-99f2-49dc-a161-e12bf2fdc1f3",
  "typ": "Bearer",
  "azp": "myclient",
  "sid": "70a67a84-f849-4b43-a3f8-b5d42b1f4daa",
  "acr": "1",
  "allowed-origins": [
    "http://localhost:8083"
  ],
  "realm_access": {
    "roles": [
      "default-roles-oauthtests",
      "offline_access",
      "uma_authorization"
    ]
  },
  "resource_access": {
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },
  "scope": "openid profile email",
  "email_verified": true,
  "name": "ivan Storozhev",
  "spring_sec_roles": [
    "default-roles-oauthtests",
    "offline_access",
    "uma_authorization"
  ],
  "preferred_username": "kudzip",
  "given_name": "ivan",
  "family_name": "Storozhev",
  "email": "test@test.ru"
}

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

Для чего нужны Access и Refresh токены?

Они служат для управления доступом к ресурсам и поддержания сеансов пользователей.

Access-токен

  1. Предоставление доступа: Access-токен предоставляет клиентскому приложению права доступа к защищённым ресурсам от имени пользователя или сервиса. Он содержит информацию о пользователе и правах доступа.

  2. Краткосрочный срок действия: Обычно имеют короткий срок действия (несколько минут или часов) для снижения риска компрометации.

  3. Использование в API-запросах: Access-токены передаются в заголовках запросов к API, чтобы сервер мог подтвердить, что запрос авторизован.

  4. JSON Web Tokens (JWT): Часто реализуются в формате JWT, что позволяет серверам быстро проверять подпись и данные токена без дополнительного обращения к серверу авторизации.

Refresh-токен

  1. Обновление access-токена: Refresh-токен используется для получения нового access-токена, когда старый истекает, без необходимости повторной аутентификации пользователя.

  2. Долгосрочный срок действия: Имеют более длительный срок действия по сравнению с access-токенами (дни или недели), что позволяет поддерживать долгосрочные сеансы.

  3. Хранение и безопасность: Должны храниться надежно, так как компрометация refresh-токена может позволить злоумышленнику получать новые access-токены без ведома пользователя.

  4. Не передаются с каждым запросом: В отличие от access-токенов, refresh-токены не используются в обычных API-запросах, а только для обновления access-токенов.

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

Иногда нужно добавить определенную роль, чтобы доступ к странице был только для ограниченных пользователей. Такое можно сделать во вкладке Realm roles. Я добавлю роль менеджера «ROLE_MANAGER».

Создание новой роли

Создание новой роли

Создание новой роли

Даём название нашей роли

Даём название нашей роли

Теперь у нас есть отдельная роль для менеджеров. В конфиге spring security мы пропишем безопасность с учетом ролей.

Подключение авторизации и регистрации через сторонние API. Технология Oauth достаточно удобна и популярна, ее используют сейчас практически везде и с помощью Keycloak, можно подключить авторизацию и регистрацию через Google, GitHub и др. сервисы. Я воспользуюсь API только Google и GitHub. Давайте посмотрим как это сделать.

Google

Чтобы подключить Google нужно перейти на сайт с API. Здесь нам нужно создать новый Oauth client ID.

Создание нового Oauth client ID

Создание нового Oauth client ID

Откроется окно по созданию client. Первым делом нам нужно выбрать тип приложения (я выбрал Web application). Далее нужно дать имя нашему client. После нужно указать Redirect URI. Как получить Redirect URI описано в пункте Получение Redirect URI.

Заполнение полей

Заполнение полей

Получение Redirect URI
Поиск Redirect URI 

Поиск Redirect URI 

После нажатия на «Add provider» будет выбор из нескольких провайдеров, нам нужно выбрать Google.

Копирование Redirect URI

Копирование Redirect URI

Когда мы создали наш Oauth client ID, нужно зайти в него, чтобы узнать Client ID и Client secret.

Переход к нашему Client

Переход к нашему Client

Узнаем наши Client ID и Client secret

Узнаем наши Client ID и Client secret

Нам нужно скопировать эти 2 поля и перейти к Keycloak. Здесь нам нужно во вкладке, где мы копировали Redirect URI вставить недостающие поля.

Client ID — скопированный Client ID с API Google.

Client Secret — скопированный Client Secret с API Google.

Заполнение полей

Заполнение полей

GitHub

Чтобы подключить GitHub нужно перейти на сайт Developer settings. Здесь нам нужно создать new Oauth app.

Создание new Oauth app

Создание new Oauth app

Далее заполняем поля.

Заполнение полей

Заполнение полей

Получение Redirect URI
Поиск Redirect URI 

Поиск Redirect URI 

После нажатия на «Add provider» будет выбор из нескольких провайдеров, нам нужно выбрать GitHub.

Копирование Redirect URI

Копирование Redirect URI

Заходим в созданный нами Oauth app, чтобы узнать Client ID и Client secret.

Узнаем наши Client ID и Client secret

Узнаем наши Client ID и Client secret

Нам нужно скопировать эти 2 поля и перейти к Keycloak. Здесь нам нужно во вкладке, где мы копировали Redirect URI вставить недостающие поля.

Client ID — скопированный Client ID с GitHub.

Client Secret — скопированный Client Secret с GitHub.

Заполнение полей

Заполнение полей

Настройка Keycloak в Spring

Этот модуль посвящен интеграции Keycloak в Spring приложение.

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

application.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:9090/realms/OauthTests
      client:
        provider:
          keycloak:
            issuer-uri: http://localhost:9090/realms/OauthTests
            user-name-attribute: preferred_username
        registration:
          keycloak:
            client-id: myclient
            client-secret: YOUR SECRET
            scope: openid
server:
  port: 8083
logging:
  level:
    org.springframework.security: TRACE

client-id — это client id, созданного нами client в Keycloak.

client-secret — это секретный ключ, созданного нами client в Keycloak. (Мы его использовали выше, для теста пользователя)

Далее нам нужно создать класс SecurityConfig, чтобы настроить доступ к URI и реализовать oauth2 Resource Server. Это прописывается в методе securityFilterChain.

SecurityConfig.java

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
                .authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers("/error").permitAll()
                        .requestMatchers("/manager.html").hasRole("MANAGER")
                        .anyRequest().authenticated()
                )
                .oauth2ResourceServer((oauth2) -> oauth2
                        .jwt(Customizer.withDefaults())
                )
                .oauth2Login(Customizer.withDefaults());
        return http.build();
    }
}

@Configuration: Указывает, что класс содержит определения бинов и может быть использован контейнером Spring для генерации бинов.

@EnableWebSecurity: Включает поддержку безопасности веб-приложений в Spring Security.

authorizeHttpRequests: Определяет правила авторизации для HTTP-запросов.

.requestMatchers (»/error»).permitAll (): Позволяет доступ ко всем запросам на /error без аутентификации.

.requestMatchers (»/manager.html»).hasRole («MANAGER»): Разрешает доступ к /manager.html только пользователям с ролью MANAGER.

.anyRequest ().authenticated (): Все остальные запросы требуют аутентификации.

oauth2ResourceServer: Настраивает приложение как OAuth2 ресурсный сервер, используя JWT для проверки токенов.

.jwt (Customizer.withDefaults ()): Указывает на использование JWT с настройками по умолчанию.

oauth2Login: Включает OAuth2 логин с настройками по умолчанию, позволяя пользователям входить в систему с помощью провайдера OAuth2.

Далее создадим метод jwtAuthenticationConverter. Он конвертирует JWT в объект аутентификации, позволяя извлекать и преобразовывать роли из токена.

jwtAuthenticationConverter ()

@Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtAuthenticationConverter.setPrincipalClaimName("preferred_username");
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> {
            var authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
            var roles =jwt.getClaimAsStringList("spring_sec_roles");

            return Stream.concat(authorities.stream(),
                    roles.stream()
                            .filter(role -> role.startsWith("ROLE_"))
                            .map(SimpleGrantedAuthority::new)
                            .map(GrantedAuthority.class::cast))
                    .toList();
        });
        return jwtAuthenticationConverter;
    }

.setPrincipalClaimName («preferred_username»): Указывает, что preferred_username будет использоваться как имя пользователя.

jwtGrantedAuthoritiesConverter: Конвертирует роли из JWT в объекты GrantedAuthority.

roles: Извлекает роли из токена, находящегося в поле spring_sec_roles.

Stream.concat: Объединяет роли из токена и стандартные авторитеты, преобразуя их в список авторитетов.

oAuth2UserService ()

@Bean
    public OAuth2UserService oAuth2UserService() {
        var oidcUserService = new OidcUserService();
        return userRequest -> {
            var oidcUser = oidcUserService.loadUser(userRequest);
            var roles = oidcUser.getClaimAsStringList("spring_sec_roles");
            var authorities = Stream.concat(oidcUser.getAuthorities().stream(),
                            roles.stream()
                                    .filter(role -> role.startsWith("ROLE_"))
                                    .map(SimpleGrantedAuthority::new)
                                    .map(GrantedAuthority.class::cast))
                    .toList();

            return new DefaultOidcUser(authorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
        };
    }

OidcUserService: Сервис для обработки аутентификации OpenID Connect.

roles: Извлекает роли из аутентификационного ответа OpenID Connect.

Stream.concat: Объединяет роли из OpenID Connect и стандартные авторитеты, создавая новый объект DefaultOidcUser с обновленным списком авторитетов.

Целый код класса SecurityConfiig.

SecurityConfig.java

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
                .authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers("/error").permitAll()
                        .requestMatchers("/manager.html").hasRole("MANAGER")
                        .anyRequest().authenticated()
                )
                .oauth2ResourceServer((oauth2) -> oauth2
                        .jwt(Customizer.withDefaults())
                )
                .oauth2Login(Customizer.withDefaults());
        return http.build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtAuthenticationConverter.setPrincipalClaimName("preferred_username");
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> {
            var authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
            var roles =jwt.getClaimAsStringList("spring_sec_roles");

            return Stream.concat(authorities.stream(),
                    roles.stream()
                            .filter(role -> role.startsWith("ROLE_"))
                            .map(SimpleGrantedAuthority::new)
                            .map(GrantedAuthority.class::cast))
                    .toList();
        });
        return jwtAuthenticationConverter;
    }

    @Bean
    public OAuth2UserService oAuth2UserService() {
        var oidcUserService = new OidcUserService();
        return userRequest -> {
            var oidcUser = oidcUserService.loadUser(userRequest);
            var roles = oidcUser.getClaimAsStringList("spring_sec_roles");
            var authorities = Stream.concat(oidcUser.getAuthorities().stream(),
                            roles.stream()
                                    .filter(role -> role.startsWith("ROLE_"))
                                    .map(SimpleGrantedAuthority::new)
                                    .map(GrantedAuthority.class::cast))
                    .toList();

            return new DefaultOidcUser(authorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
        };
    }
}

На этом самая базовая настройка Keycloak закончена. Теперь мы можем перейти к тестам.

Тестирование

Для проверки создадим две html страницы. Первая страница будет доступна для всех авторизированных пользователей authenticated.html, а вторая будет видна только менеджерам manager.html. Это сделано для того, чтобы протестировать, работает ли разделение по ролям.

authenticated.html




    


  

Привет, аутентифицированный пользователь

manager.html




    


  

Hi manager

Далее запустим наш проект и перейдем на наш хост — http://localhost:8083.

Запуск проекта

Запуск проекта

Переход на URI

Переход на URI

Как видно, нас сразу же перебрасывает на форму авторизации Keycloak. В этой форме есть вход через username/email, а также через GitHub и Google. Попробуем войти под нашим пользователем.

Попытка входа

Попытка входа

Мы смогли зайти под нашим пользователем. Так как мы создали authenticated страничку, то давайте попробуем туда зайти, указав такой URI — http://localhost:8083/authenticated.html.

5cf9dacbb9c1bc70461a1cba6dd3a9f1.png

Попробуем зайти на страничку manager.

manager.html

manager.html

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

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

Добавление роли

Добавление роли

Выбор нашей роли

Выбор нашей роли

Далее выбираем фильтр «by realm roles» и тут выбираем нашу роль, которую мы создали ранее. Все, новая роль присвоена пользователю. Перезапустим приложение и попробуем войти заново на страничку менеджера.

manager.html

manager.html

Как видно, все работает!

Теперь проверим вход и регистрацию через Google и GitHub.

Так как мы не сделали страничку выхода, то сессию нужно завершить в самом Keycloak и перезапустить наше приложение. В Keycloak переходим во вкладку Users, выбираем нашего пользователя, переходим в сессии и завершаем их все.

Завершение сессии

Завершение сессии

Google

Далее вновь заходим на наш хост и в форме авторизации выбираем Google.

Авторизация через Google

Авторизация через Google

Далее выбираем наш аккаунт.

Вход через Google

Вход через Google

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

Users

Users

GitHub

Далее вновь заходим на наш хост и в форме авторизации выбираем GitHub.

Авторизация через GitHub

Авторизация через GitHub

Далее вводим наши данные от аккаунта.

Вход через GitHub

Вход через GitHub

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

Users

Users

Из тестирования видно, что все работает. Авторизация проходит по логину и паролю, а также через Google и GitHub.

Вывод

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

  1. Создание своего realm.

  2. Создание нового client.

  3. Создание своей роли.

  4. Подключение сторонних API.

Данная инструкция поможет освоить Keycloak.

Весь код вы можете найти в моем GitHub.

© Habrahabr.ru