Сын Маминой Подруги
Сын Маминой Подруги

Хочешь облегчить себе жизнь и сэкономить время?

Привет! Если ты так же как и я решил использовать keycloak для аутентификации и авторизации в своей микро‑сервисной архитектуре, то я расскажу вам как правильно настроить сам keycloak, его рабочую среду а в конце мы подключим Active Directory к нашему приложению. Перед прочтением данного гайда прошу ознакомиться с keycloak по данной ссылке.

Стек Технологии

Я Джавист от кожи до костей и именно поэтому все микро‑сервисы у нас будут написаны в связке Java 18 и Spring Boot. Сборник проекта у меня Maven, так как довольно простой в своем применении.

Информация по другим сервисам:

  • PostGreSql — база данных;

  • PgAdmin — GUI БД;

  • Сервис Аутентификации — KeyCloak V19;

  • Zipkin + Sleuth — технология трассировки запросов;

  • Kafka — Брокер Сообщений;

  • Eureka Server — регистрация наших микро‑сервисов.

Готовый docker‑compose файл:

services:
  postgres:
    container_name: postgres-gilgamesh
    image: postgres
    environment:
      POSTGRES_USER: "здесь ваш username"
      POSTGRES_PASSWORD: "здесь ваш password"
      POSTGRES_HOST_AUTH_METHOD: trust
      POSTGRES_DB: keycloak_db
    volumes:
      - postgres:/data/postgres
    ports:
      - "5432:5432"
    networks:
      - postgres
    restart: unless-stopped
  keycloak:
    image: quay.io/keycloak/keycloak:legacy
    platform: linux/arm64
    environment:
      DB_VENDOR: POSTGRES
      DB_ADDR: postgres
      DB_SCHEMA: public
      DB_DATABASE: keycloak_db
      DB_USER: "здесь ваш username от БД"
      DB_PASSWORD: "здесь ваш password от БД"
      KEYCLOAK_USER: "здесь ваш username"
      KEYCLOAK_HOSTNAME: localhost
      KEYCLOAK_PASSWORD: "здесь ваш пароль"
    ports:
      - 8082:8080
    depends_on:
      - postgres
    networks:
      - postgres

  #Образ Готового LDAP если у вас нет тестового варианта
  ldap:
    image: rroemhild/test-openldap
    ports:
      - 10389:10389
      - 10636:10636
    networks:
      - postgres


  pgadmin:
    container_name: pgadmin-gilgamesh
    image: dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org}
      PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin}
      PGADMIN_CONFIG_SERVER_MODE: 'False'
    volumes:
      - pgadmin:/var/lib/pgadmin
    ports:
      - "5050:80"
    networks:
      - postgres
    restart: unless-stopped

  zipkin:
    image: openzipkin/zipkin
    container_name: zipkin-gilgamesh
    ports:
      - "9411:9411"
    networks:
      - spring


  zookeeper:
    image: confluentinc/cp-zookeeper:latest
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    ports:
      - 22181:2181

  kafka:
    image: confluentinc/cp-kafka:latest
    depends_on:
      - zookeeper
    ports:
      - 29092:29092
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1

networks:
  postgres:
    driver: bridge
  spring:
    driver: bridge
volumes:
  postgres:
  pgadmin:

Настройка и конфигурация нашего parent pom.xml

Данный проект будет работать локально и все микро‑сервисы будут находиться в одной директории. Благодаря этому мы можем использовать особенность maven'a в которой мы можем указать parent файл, где будут лежать наши основные зависимости, которые нам не нужно будет прописывать по несколько раз.

В моем проекте будет несколько модулей, давайте сразу же их укажем в нашем parent pom.xml файле:

<modules>
        <module>eureka-server</module>
        <module>incidents</module>
        <module>apigw</module>
        <module>clients</module>
        <module>kafka</module>
        <module>authentication</module>
</modules>

У вас эти модули могут отличаться от моих, так как у вас может быть совершенно другая архитектура с другими сервисами.

Для управления сторонними зависимостями у мавена есть специальная секция dependencyManagement и механизм наследования.

Благодаря dependencyManagement в maven мы можем добавлять родительские зависимости в наш проект, благодаря которым, мы сможем использовать весь функционал его наследников. Выглядит это примерно вот так:

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>2.6.7</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.6.7</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
            <dependency>
                <groupId>org.keycloak.bom</groupId>
                <artifactId>keycloak-adapter-bom</artifactId>
                <version>20.0.3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

"Дополнительно" Автоматическое обертывание сервиса в докер

Для автоматическое обертывание образов в докер я использую мой любимый JIB.

Jib создает оптимизированные образы Docker и OCI для ваших Java‑приложений без использования демона Docker — и без глубокого освоения лучших практик Docker. Он доступен в виде плагинов для Maven и Gradle, а также в виде библиотеки Java.

Основные цели JIB

  • Быстрота — Быстрое развертывание изменений. Jib разделяет ваше приложение на несколько слоев, отделяя зависимости от классов. Теперь вам не нужно ждать, пока Docker пересоздаст все ваше Java‑приложение — достаточно развернуть только те слои, которые изменились.

  • Воспроизводимость — при пересборке образа контейнера с тем же содержимым всегда создается один и тот же образ. Никогда больше не вызывайте ненужных обновлений.

  • Без демона — Сократите количество зависимостей от CLI. Создайте свой образ Docker из Maven или Gradle и отправьте его в любой реестр по вашему выбору. Больше не нужно писать Docker‑файлы и вызывать docker build/push.

Более подробно о JIB можно почитать вот здесь: https://habr.com/ru/post/552 494/. В моем случае конфигурация выглядит вот так:

<build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <version>2.6.7</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>repackage</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>com.google.cloud.tools</groupId>
                    <artifactId>jib-maven-plugin</artifactId>
                    <version>3.3.0</version>
                    <configuration>
                        <from>
                            <image>eclipse-temurin:17</image>
                            <platforms>
                                <platform>
                                    <architecture>arm64</architecture>
                                    <os>linux</os>
                                </platform>
                                <platform>
                                    <architecture>amd64</architecture>
                                    <os>linux</os>
                                </platform>
                            </platforms>
                        </from>
                        <to>
                            <tags>
                                <tag>latest</tag>
                            </tags>
                        </to>
                    </configuration>
                    <executions>
                        <execution>
                            <phase>package</phase>
                            <goals>
                                <goal>
                                    build
                                </goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

Настройка простого сервиса

Давайте создадим простой сервис под названием Incident и добавим в него несколько роутов и засекюрим их при помощи keycloak.

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>gilgamesh_project</artifactId>
        <groupId>com.project</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>
    <artifactId>incidents</artifactId>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <id>build-docker-image</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>com.google.cloud.tools</groupId>
                        <artifactId>jib-maven-plugin</artifactId>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-sleuth-zipkin</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.project</groupId>
            <artifactId>kafka</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

</project>

application.yml файл:

server:
  port: 8080
spring:
  application:
    name: incidents
  datasource:
    password: password
    url: jdbc:postgresql://localhost:5432/incident-service
    username: postgres
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        format_sql: true
    show-sql: true
  zipkin:
    base-url: http://localhost:9411
  kafka:
    bootstrap-servers: localhost:29092
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
    fetch-registry: true
    register-with-eureka: true

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

@RestController
@RequestMapping("api/v1/incident/service")
@RequiredArgsConstructor
public class IncidentController {

    private final IncidentService incidentService;

    @PostMapping("/create")
    public ResponseEntity<IncidentCreateDtoResponse> create(@Valid @RequestBody IncidentDtoRequest incidentDtoRequest) {
        IncidentCreateDtoResponse incidentDtoResponse = IncidentCreateMapper.incidentCreateToDto(incidentService.create(incidentDtoRequest),new ArrayList<>());
        return new ResponseEntity<>(incidentDtoResponse, HttpStatus.OK);
    }
}

Замечательно, мы настроили наш сервис, теперь давайте подключим к нему keycloak.

Настройка и запуск KeyCloak

Откройте url по который вы указали в переменных keycloak в docker‑compose. Вас должна встретить следующая картина:

Если вы увидели данное окно, то поздравляю, вы успешно запустили keycloak. Теперь нажмите на admin console и авторизируйтесь в систему под данными, которые вы вводили в docker compose файл.

Дальше делайте все по этому алгоритму действий:

  1. Создайте новый realm для вашего проекта.

  1. Создайте новый client для вашего нового realm.

  1. Создайте несколько тестовых пользователей и навесьте на них несколько тестовых ролей. В моем случае это пользователи:

    1. username: arnur, roles: ["USER"]

    2. username: adal, roles: ["USER"]

  2. Теперь добавьте credentials вашего keycloak в ваш application.yml, чтобы в дальнейшем наше Spring Boot приложение могло генерировать токены и авторизировать пользователя:

keycloak:
  auth-server-url: http://localhost:"ваш порт"/auth
  resource: "название вашего клиента"
  bearer-only: true
  public-client: true
  realm: "название вашего реалма"
  1. Теперь наступает время настройки вашего spring security. Для этого при создании вашей конфигурации вы будете наследоваться от класса KeycloakWebSecurityConfigurerAdapter. Данный класс позволяет нам создавать сессии и конфигурировать защиту наших роутов. Пример кода:

@KeycloakConfiguration
@Import(KeycloakSpringBootConfigResolver.class)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
    /**
     * Registers the KeycloakAuthenticationProvider with the authentication manager.
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = new KeycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    /**
     * Defines the session authentication strategy.
     */
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(buildSessionRegistry());
    }

    @Bean
    protected SessionRegistry buildSessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        super.configure(http);
        http.cors().and().csrf().disable();
        http.authorizeRequests()
                .antMatchers("/api/v1/incident/service/**").hasRole("USER");
        http.authorizeRequests().anyRequest().permitAll();
    }
}

Поздравляю теперь вы можете проходить авторизацию при помощи oauth2.0 токена, через keycloak. Чтобы проверить это используйте postman и выберите авторизацию через токен. Вот самая дефолтная настройка:

Подключение LDAP к вашему проекту

Теперь начинается самый сок! KeyCloak позволяет нам использовать пользователей из AD для авторизации. Для примера я буду использовать готовый образ LDAP, который вы можете скачать по этой ссылке: https://hub.docker.com/r/rroemhild/test-openldap/. Делайте пул и разворачивайте в докере, мы живем в 21ом веке!

Чтобы добавить LDAP к вашему Keyсloak зайдите в раздел "User Federation" и выберите LDAP как ваш "User Provider" и укажите все согласно инструкции:

  • Console display name: какой душе угодно;

  • Connection URL: тут если вы развернули ldap через докер url будет такой: ldap://»ip вашего компа»:10 389. Почему‑то keycloak напрочь отказывается видеть ваш ldap через localhost даже если вы внедрите их в один docker‑network;

  • Bind Type: указывает BN вашего админа в случае тестового LDAP это: cn=admin,dc=planetexpress,dc=com

  • Bind credentials: указывает пароль вашего админа в случае тестового LDAP это: GoodNewsEveryone

  • EDIT MODE: советую ставить READ_ONLY

  • Users DN: указывает общую инфу вашего рядового пользователя в случае тестового LDAP это: ou=people,dc=planetexpress,dc=com

  • Username LDAP attribute: Имя атрибута LDAP, который отображается как имя пользователя Keycloak. Для нашего тестового LDAP это: uid

  • RDN LDAP attribute: Имя атрибута LDAP, который используется в качестве RDN (верхнего атрибута) типичного DN пользователя. Для нашего тестового LDAP это: uid

  • UUID LDAP attribute: Имя атрибута LDAP, который используется в качестве уникального идентификатора объекта (UUID) для объектов в LDAP. Для нашего тестового LDAP это: entryUUID

  • User object classes: Все значения атрибута LDAP objectClass для пользователей в LDAP, разделенные запятыми. Для нашего тестового LDAP это: inetOrgPerson, organizationalPerson, person, top

После этого нажмите save и в action выберите sync all users. После этого все юзеры из ldap должны синхронизироваться с вашим keycloak клиентом.

Дальше зайдите во вкладку Users и вы должны наблюдать примерно такую картину:

Теперь попробуйте задать некоторым пользователям созданную вами до этого роль (в моем случае это USER). И попробуйте сгенерировать токен. Если у вас все получилось, то поздравляю, вы успешно настроили KeyCloak как сервис аутентификации.

Итоги

Keycloak супер универсальный сервис авторизации и аутентификации. Он сильно экономит время разработки и позволяет настраивать систему авторизации как душе угодно. Keycloak так же крайне просто конфигурируется и запускается на любоей системе, а так же имеет открытый исходный код. А самое главное, он написан на JAVA!

Всем добра и позитива! Пользуйтесь Kcell и Activ!

Комментарии (12)


  1. KirasiH
    00.00.0000 00:00
    -4

    Норм статья


  1. OMFGDom
    00.00.0000 00:00
    +1

    Спасибо за туториал, давно искал подробные гайды без воды по KeyCloak, добавил в закладки, автору респект


  1. wwalex101
    00.00.0000 00:00
    +1

    если будет апи для фронта все же нужно будет в jwt делать преобразование со всеми вытекающими


  1. ggo
    00.00.0000 00:00

    Раз уж мы говорим про микросервисы, то для полноты картины, можно еще добавить OAuth API Gateway, например в лице Gogatekeeper.


    1. LordBarov Автор
      00.00.0000 00:00

      Приветствую! Такие компоненты как AG и брокера сообщений оставил за ширмой, чтобы уделить больше времени на KeyCloak. А так каждый разраб, который работал с микрашами, думаю знает об AG


  1. Amikuto
    00.00.0000 00:00
    +1

    Самая первая ссылка на ознакомление с keycloak не работает(


    1. LordBarov Автор
      00.00.0000 00:00

  1. Enrey
    00.00.0000 00:00

    Не смотря на позитивные упоминания docker, imho слишком много ручной работы по конфигурированию keycloak/realm/ldap/...

    Жду продолжения статьи с автоматическим конфигурированием keycloak при старте docker-контейнера https://github.com/adorsys/keycloak-config-cli :)


  1. 1q2w1q2w
    00.00.0000 00:00

    Пара вопросов по статье:

    1) в родительских зависимостях указан адаптер кейклок. Сам кейклок уже давно задепрекейтил все свои адаптеры, оставив по сути сам Auth server, а клиенты должны использоваться либо самописные либо сторонние, в случае с java это преимущественно spring auth2 client. Вообще была занятная история про адаптеры(ссылки легко можно найти): сначала разработчики spring решили выпилить auth server и объявили об этом сообществу. С рекомендацией переходить на сторонние решения вроде кейклок. Сообществу это не понравилось и они объявили протест, в связи с чем разработчики спринга выпустили свой новый authorization server(кажется в конце 2021 г). А уже в феврале 2022 кейклок обязал переходить с их адаптеров на сторонние клиенты. Да, знаю речь про разные части - Auth server и клиент, но по хронологии получается так что кейклок решил отдать поддержку клиентов на откуп сторонним разработчикам, а самим сосредоточиться на сервере. Т.е. если я все понял правильно клиентскую часть в самом приложении нужно переписать на spring auth2 client либо resource server если там будет только авторизация.

    2) не хватает ссылочки на гитхаб, чтобы можно былаи понятна общая картина, сейчас отдельные части вроде eureka/sleuth упомянутые в статье оторваны от контекста(либо предполагается вторая часть с углубленным разбором cloud составляющей?)

    3) просто вопрос: насколько сложно было бы заменить standalone keycloack на embedded в spring boot ? Возможность иметь сервер авторизации в обычном java приложении, поддерживаемом командой разработки видится заманчивой.

    Спасибо.


    1. 1q2w1q2w
      00.00.0000 00:00

      И поясните пожалуйста механизм работы кейклок с сессиями, упомянутыми в статье. Просто токены доступа(jwt или opaque) все таки никак не связаны с традиционными сессиями. Имеется ввиду что кейклок просто поддерживает состояние клиентов через выданные токены в виде сессий или это именно cookie с которыми клиенты делают запросы?


    1. tia_ru
      00.00.0000 00:00

      Внесу уточнения.
      spring auth2 client (spring-security-oauth2-client) - для аутентификации к внешним сервисам по OAuth2.0/OIDC.
      Keyclok Adapters - для авторизации входящих http-запросов по JWT-токену
      Вместо Keyclok Adapters лучше использовать spring-security-oauth2-resource-server из состава Spring Security. Полагаю, что именно появление поддержки OAuth2.0/OIDC в Spring Security дало основание разработчикам Keycloak отказаться от развития Keyclok Adapters для Java.


  1. tia_ru
    00.00.0000 00:00

    У Keycloak 19 версии есть проблема -- работает только одна настройка "LDAP User Federation". Я не отслеживал, исправили ли её в последующих версиях. Для устранения этой проблемы мне в своё время пришлось написать плагин к Keycloak https://github.com/tia-ru/keycloak-md-ldap.
    Плагин также позволяет интегрировать Keycloak с несколькими независимыми доменами AD (без взаимного доверия)