KeyCloak и микро-сервисы. Как облегчить жизнь программисту

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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

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

Привет! Если ты так же как и я решил использовать keycloak для аутентификации и авторизации в своей микро-сервисной архитектуре, то я расскажу вам как правильно настроить сам keycloak, его рабочую среду а в конце мы подключим Active Directory к нашему приложению. Перед прочтением данного гайда прошу ознакомиться с keycloak по данной ссылке: https://habr.com/ru/company/southbridge/blog/654475/

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

Я Джавист от кожи до костей и именно поэтому все микро-сервисы у нас будут написаны в связке 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/552494/. В моем случае конфигурация выглядит вот так:

<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 и выберите авторизацию через токен. Вот самая дефолтная настройка:

  • Access Token URL: http://"ваш урл"/auth/realms/"ваш реалм"/protocol/openid-connect/token

  • Client ID: наименование вашего клиента

  • grant type: password credentials

  • username: username пользователя

  • password: пароль пользователя

  • scope: openid

Подключение 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 вашего компа":10389. Почему-то 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!

Источник: https://habr.com/ru/post/720070/


Интересные статьи

Интересные статьи

Мы в последнее время много рассказывали про страны, куда специалисту из-за границы или практически не попасть, или ехать особо нет смысла, разве что за интересным проектом. Решили исправиться и расска...
К написанию этой публикации меня побудила следующая статья на Хабре: «Математика провисающих проводов и цепей в играх», хоть я и совершенно не занимаюсь играми. Статья заинтересова...
К 35 годам я начал задумываться о том, на каких принципах строить жизнь, чтобы больше времени чувствовать себя счастливым. Я почитал немного про стоицизм, буддизм, экзист...
Последние несколько недель я работал над своим сайтом и хотел придать ему некоторый динамизм. Эта статья не о создании веб-страницы. Я покажу готовые сниппеты с объяснениями. ...
Эта публикация написана после неоднократных обращений как клиентов, так и (к горести моей) партнеров. Темы обращений были разные, но причиной в итоге оказывался один и тот же сценарий, реализу...