Строим свой SSO. Часть 3: Redis, Swagger, Vue.js

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

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

Список статей этой серии

  • Часть 1: Строим свой SSO сервер используя Spring Authorization Server

  • Часть 2: Строим свой SSO. PostgreSQL и ролевая модель

  • Часть 3: Строим свой SSO. Часть 3: Redis, Swagger, Vue.js

Вступление

Всем привет, мы продолжаем строить собственный SSO Server. В предыдущей статье мы разобрали:

  • Построение Resource Server для нашего SSO.

  • Подключили СУБД PostgreSQL к проекту и настроили механизмы раската схемы БД.

  • Создали объектно-реляционное отображение таблиц БД и сделали необходимые правки по коду.

  • Рассмотрели три типа ролевой модели и добавили ее в проект.

  • Создали роли и привилегии, протестировали механизмы разграничения доступа Spring Security.

Из запланированного (см. введение предыдущей статьи) нам осталось:

  • Подключить Redis

  • Настроить Swagger

  • Создать кастомизированный интерфейс для j-sso с использованием Vue.js

В этой статье мы разберем эти три пункта. Приступим!

Раздел 3.1: Добавляем Redis

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

  1. Надежность - система должна продолжать работать корректно (осуществлять нужные функции на требуемом уровне производительности) даже при неблагоприятных обстоятельствах (в случае аппаратных или программных сбоев либо ошибок пользователя).

  2. Масштабируемость - должны быть предусмотрены разумные способы решения возникающих при росте системы проблем (объемов данных, трафика или сложности).

  3. Удобство сопровождения - необходимо обеспечить возможность эффективной работы с системой множеству различных людей (разработчикам и обслуживающему персоналу, занимающимся как поддержкой текущего функционирования, так и адаптацией системы к новым сценариям применения).

В понятие надежность также входит понятие устойчивость к аппаратным сбоям. Чтобы было понятно, примером этого могут быть фатальные сбои винчестеров, появление дефектов ОЗУ, отключение электропитания, выход из строя виртуалки, отключение кем-то не того сетевого кабеля и т.д. А в понятие масштабируемость входит понятие горизонтальная масштабируемость, как средство решения проблем при возросшей нагрузке. Для обоих случаев простейшим решением становиться избыточность экземпляров запущенных приложений. Это означает, что наш j-sso в продуктивной среде будет запущен как минимум в двух, а то и более экземплярах (см. схему ниже).

Пример продуктивной среды j-sso
Пример продуктивной среды j-sso

Соответственно, когда мы это поняли, вспомним, что j-sso, а именно Spring Security и ижи с ними, внутри себя хранит токены доступа и сессии авторизовавшихся пользователей. Если у нас несколько экземпляров j-sso, то на каком из них буду храниться эти данные в определенный момент времени? И во вторых, на какой экземпляр мы попадем после балансировщика нагрузки?

Все парильно, на данный момент эти данные будут "размазаны" по всем экземплярам, а предсказать на какой экземпляр попадет запрос невозможно. Поэтому у нас и появилась задача сложить эти данные в какое-нибудь In Memory Data Grid хранилище, а именно в Redis. Конечно, мы могли бы использовать и другие IMDG хранилища, но так как я на большинстве проектов его использую, а Spring предоставляет всеобъемлющую поддержку работы с Redis, то я выбрал его.

Итак, давайте уже подключим этот Redis и заставим Spring Security работать с ним.

Первым делом добавим в наш docker-compose.yml файл сервис j-redis, чтобы было с чем тестироваться.

docker-compose.yml


version: '3.3'
services:
  // .....

  j-redis:
    image: redis:7.2-rc2
    restart: always
    command: redis-server --save 20 1 --loglevel warning --requirepass qwerty12345678
    ports:
      - '6379:6379'
    networks:
      - j-network

  // .....

Далее, нам понадобятся следующие зависимости:

  • spring-boot-starter-data-redis - нужна для организации подключения j-sso к Redis.

  • spring-session-data-redis - нужна, чтобы заставить хранить данные Spring Session в Redis

j-sso/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    // .....
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
    </dependencies>
    // .....

</project>

И наконец, достаточно добавить следующие настройки в наш application.yml. После чего можно проверять:

application.yml

spring:
  application:
    name: j-sso
  session:
    timeout: 1800                 # Время жизни сессии (в секундах)
    redis:
      flush-mode: on_save         # Указываем, когда изменения сеанса записываются в хранилище (immediate или on_save)
      namespace: j-sso:session    # Пространство имен для ключей, используемых для хранения сессий.
      save-mode: on_set_attribute # Определяет в какой момент происходит сохранение изменений сессии (on_set_attribute, on_get_attribute, always)
  data:
    redis:                          # Настраиваем подключение к Redis
      client-type: lettuce        # Указываем тип клиента Redis (jedis или lettuce)
      database: 0                 # Указываем номер БД Redis
      host: localhost             # Хост подключения
      port: 6379                  # Порт подключения
      password: qwerty12345678    # Пароль подключения
      lettuce:                      # Настраиваем пул подключений клиента lettuce
        pool:
          max-active: 16          # Максимальное количество подключений в пуле
          max-idle: 8             # Минимальное количество "idle" подключений в пуле

Все настройки достаточно простые, для получения дополнительных сведений можно обратиться к документации. Параметры Spring Session смотрите здесь. Параметры Spring Data Redis смотрите здесь.

Единственное, можно обсудить почему lettuce, а не jedis. Во-первых, потому что он используется по умолчанию (данный клиент поставляется в составе зависимости spring-session-data-redis). А во-вторых, в нашем приложении нет каких-то весомых причин не использовать или использовать lettuce. На самом деле jedis точно также мог быть использован, как и lettuce.

  • Lettuce — это полностью неблокирующий Java-клиент Redis. Он поддерживает как синхронную, так и асинхронную связь. Его сложные абстракции позволяют легко масштабировать продукты.

  • Jedis — это клиентская библиотека внутри Redis, разработанная для повышения производительности и простоты использования. Он предлагает меньше функций, но по-прежнему может обрабатывать большие объемы памяти.

По факту, клиент lettuce, более продвинутый за счет поддержки асинхронной связи и возможности его использования в реактивных приложениях. Но он точно также может быть использован в более простых приложениях с использованием синхронной связи с Redis.

Теперь запустим приложение (не забудьте запустить postgresql и redis). Далее, пройдем аутентификацию любым способом и заглянем в хранилище Redis.

Данные в Redis после аутентификации
Данные в Redis после аутентификации

Как видно, под ключом с идентификатором нашей сессии (это значение куки JSESSIONID) находиться ряд данных, среди которых есть Spring Security Context. Это означает, что все корректно сработало и теперь данные наших сессий хранятся в Redis. К тому же, если вы перезагрузите j-sso, то данные не потеряются и все активные сессии останутся активными. Учитывайте это при дальнейшей работе с проектом. Из-за этого часто разработчики некорректно тестируют свои решения или ищут часами ошибки в коде, а надо всего лишь очистить Redis.

Теперь, мы можем запустить j-sso в нескольких экземплярах и не беспокоиться о данных, так как на всех инстансах приложений они будут доступны. Плюсом ко всему вышесказанному, у нас появляется множество возможностей работы с данными сессий пользователей, которые нам пригодятся в дальнейшем.

На этом подключение Redis к проекту завершено.

Исходники данного раздела смотрите здесь.

Раздел 3.2: Поговорим о Swagger

Swagger — это набор инструментов для разработчиков API от SmartBear Software и бывшая спецификация, на которой основана спецификация OpenAPI.

По факту, у нас есть:

  1. OpenAPI 3 - спецификация, на основе которой можно задокументировать наши контроллеры и их endpoint-ы. OpenAPI Specification

  2. Swagger UI - это утилита предоставляющая интерфейс, генерируемый на основе OpenAPI. Он предоставляет возможности просмотра и взаимодействия с endpoint-ами сервисов.

Данные инструменты нам позволят легко и без лишних телодвижений документировать наш API, а также его тестировать. При этом не используя никакого больше стороннего ПО.

Для подключения Swagger в Spring Boot приложение имеется две библиотеки: Springfox и Springdoc. Springfox считается устаревшей, поэтому берем Springdoc.

Данная библиотека очень грамотно построена, она состоит из нескольких артефактов, каждый из которых отвечает за свою часть. Ниже представлена схема артефактов из их документации. Обратите внимание, что мы рассматриваем версию 2.x.x. Версия 1.7.х не будет работать со Spring Boot 3.

Схема артефактов из документации springdoc
Схема артефактов из документации springdoc

Как видно из данной схемы, springdoc имеет артефакты, которые предназначены для работы как в приложениях Spring MVC, так и артефакты для реактивных приложений с WebFlux. Нам нужны те что справа, так как мы не используем WebFlux. Также, springdoc разделяет свои артефакты на те, что предоставляют механизмы генерации документации, на основе наших контроллеров, по спецификации OpenAPI. А также артефакты, которые предоставляют вместе с этим ещё и Swagger UI, для интерактивного отображения этой документации. Соответственно, так как сейчас мы будем подключать все в j-sso, то нам понадобиться добавить следующую зависимость в наш pom.xml:

j-sso/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    // ...

    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>2.1.0</version>
    </dependency>

    // ...
</project>

Теперь необходимо создать бин класса OpenAPI в нашем RootConfig. В нем мы объявим имя, описание и версию нашего сервиса. Эта информация будет выведена на странице документации Swagger.

RootAppConfig.java


@Slf4j
@Configuration
@RequiredArgsConstructor
@EnableJpaRepositories(basePackages = "ru.dlabs.sas.example.jsso.dao.repository")
public class RootAppConfig {

    // ....

    @Bean
    public OpenAPI openAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("Наш прекрасный SSO")
                        .description("Некое описание этого SSO")
                        .version("0.0.1")
                );
    }
}

Теперь, если запустить наш j-sso то у нас есть два endpoint-а.

  • http://localhost:7777/swagger-ui/index.html - по нему будет доступен Swagger UI

  • http://localhost:7777/v3/api-docs - по нему будет доступна спецификация OpenAPI нашего сервиса в json формате

Отображение спецификации j-sso в Swagger UI
Отображение спецификации j-sso в Swagger UI

Единственное, наш SSO попросит вначале пройти аутентификацию. Это не совсем удобно, так как Swagger UI это инструмент, который необходим при разработке или тестировании, и никакого отношения к пользователю SSO не имеет. Поэтому, вынесем его endpoint-ы из-под механизмов Security. Для этого, изменим SecurityConfig и AuthorizationServerConfig следующим образом:

SecurityConfig.java


@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {

    // ...

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

        // ...

        http.authorizeHttpRequests(authorize ->
                authorize
                        // endpoint-ы swagger вынесем из под security
                        .requestMatchers(
                                "/v3/api-docs",
                                "/swagger-ui/**",
                                "/v3/api-docs/swagger-config"
                        ).permitAll()
                        .anyRequest().authenticated()
        );
        return http.formLogin(withDefaults()).build();
    }
}

AuthorizationServerConfig.java


@RequiredArgsConstructor
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {

    // ....

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {

        // .....

        RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
        http.securityMatcher(endpointsMatcher)
                .authorizeHttpRequests(authorize ->
                        authorize
                                // endpoint-ы swagger вынесем из под security
                                .requestMatchers(
                                        "/v3/api-docs",
                                        "/swagger-ui/**",
                                        "/v3/api-docs/swagger-config"
                                ).permitAll()
                                .anyRequest().authenticated()
                )
        /*...*/;
        return http.build();
    }
}

После этого, можно открыть страницу Swagger UI без всякой аутентификации.

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

application.yml

// ...

springdoc:
  swagger-ui:
    enabled: true
    use-root-path: true
    path: /swagger-ui
    doc-expansion: none
    config-url: /v3/api-docs/swagger-config
    disable-swagger-default-url: true
    operationsSorter: method
    tagsSorter: alpha
    url: /v3/api-docs
    try-it-out-enabled: true
  api-docs:
    enabled: true
    path: /v3/api-docs

  // ...
  • swagger-ui.use-root-path - выставим true, чтобы swagger-ui был доступен из корня приложения

  • swagger-ui.path - кастомизируем путь до HTML страницы swagger-ui

  • swagger-ui.doc-expansion - этим параметром можно настроить раскрытие элементов в списках. Можно сразу раскрыть все или ничего не раскрывать.

  • swagger-ui.config-url - можно кастомизировать URL до конфигурации HTML страницы swagger-ui. Мы его выставим сейчас по умолчанию

  • swagger-ui.disable-swagger-default-url - по умолчанию swagger-ui открывает страницу Pet Store (пример сервиса). Нам это не нужно, поэтому выключим

  • swagger-ui.operationsSorter - включим сортировку по HTTP методам

  • swagger-ui.tagsSorter - включим сортировку по алфавиту в наименованиях контроллеров

  • swagger-ui.url - указываем URL до спецификации OpenAPI нашего сервиса

  • swagger-ui.try-it-out-enabled - можно включить/выключить возможность сделать запрос из swagger-ui

  • api-docs.path - можно кастомизировать путь до спецификации OpenAPI нашего j-sso

В принципе, можно сказать, что мы подключили Swagger к проекту. Но это еще не все. Нам надо сделать так, чтобы была возможность пройти авторизацию и получить токен доступа. Это нужно для того чтобы было удобно делать запросы с формы swagger-ui. Во-вторых, у нас уже есть два сервиса (j-sso и j-service), а дальше будет больше. Получается, что на каждом сервисе нам надо настраивать и добавлять в сборку Swagger UI. Конечно, можно только на одном из сервисов добавить поддержку Swagger UI и в поле Explorer указывать полный путь до спецификации нужного нам сервиса. Но все это выглядит как-то не удобно.

Обычно, при разработке микросервисных решений, Swagger UI ставят только на один сервис, а именно на Gateway - это обратный прокси сервис, который можно реализовать с использованием Zuul или Spring Cloud Gateway. При этом Swagger UI можно настроить так, что на странице документации будет выводиться список доступных сервисов вместо поля "Explorer", выбирая которые будет подгружаться нужная спецификация. Хотелось бы, чтобы у нас было что-то аналогичное, но у нас нет gateway, у нас каждый сервис будет предназначен для конкретного pet-проекта и будет самостоятельным решением.

Поэтому, предлагаю сделать отдельный сервис, который будет предназначен только для запуска Swagger UI. Соответственно, создадим модуль j-swagger-ui в нашем проекте и настроим в нем простейшее Spring Boot приложение. Далее, добавим зависимость springdoc-openapi-starter-webmvc-ui. Далее, аналогично модулю j-sso, настроим swagger-ui. Единственное отличие будет в том, что мы отключим поддержку генерации спецификации OpenAPI этого сервиса при помощи настройки springdoc.api-docs.enabled = false, так как это просто не имеет смысла. Ниже показано как будет выглядеть файл application.yml этого сервиса:

application.yml

server:
  port: 7778

logging:
  level:
    root: INFO
    org.springframework.security.web.FilterChainProxy: ERROR
    logging.level.org.hibernate.SQL: INFO
    com.zaxxer.hikari: ERROR
    org.postgresql: ERROR

spring:
  application:
    name: j-swagger-ui

springdoc:
  swagger-ui:
    enabled: true
    use-root-path: true
    doc-expansion: none
    disable-swagger-default-url: true
    operationsSorter: method
    tagsSorter: alpha
    try-it-out-enabled: true
    urls:
      - name: SSO Server
        url: http://localhost:7777/v3/api-docs
  api-docs:
    enabled: true

Для решения проблемы отображения нескольких сервисов в springdoc есть следующие настройки springdoc.swagger-ui.urls[0].*. С их помощью мы можем настроить отображение документации нескольких сервисов. Теперь запустим и проверим как это работает.

Swagger UI в сервисе j-swagger-ui
Swagger UI в сервисе j-swagger-ui

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

Cross-Origin Resource Sharing (CORS) — механизм, использующий дополнительные HTTP-заголовки, чтобы дать возможность агенту пользователя получать разрешения на доступ к выбранным ресурсам с сервера на источнике (домене), отличном от того, что сайт использует в данный момент. Говорят, что агент пользователя делает запрос с другого источника (cross-origin HTTP request), если источник текущего документа отличается от запрашиваемого ресурса доменом, протоколом или портом.

Для решения этой проблемы, мы должны на запрашиваемом ресурсе разрешить выполнение запросов с домена на котором расположен наш j-swagger-ui. Для этого перейдем в j-sso и найдем бин corsFilter, в котором мы конфигурируем CORS. Нам достаточно добавить значение http://localhost:7778 в параметр AllowOrigins и все заработает. Но, так как это уже вторая ситуация добавления нового ресурса в конфигурацию CORS, то думаю пора вынести настройки cors в application.yml. Создадим класс AppProperties в котором мы будем создавать вложенные классы с различными настройками сервиса. Например, как для CORS сейчас:

AppProperties.java


@Configuration
public class AppProperties {

    @Getter
    @Setter
    @Configuration
    @ConfigurationProperties(prefix = "spring.mvc.cors")
    public static class CorsProperties {

        private List<CorsConfig> configs;

        public static record CorsConfig(
                String pattern,
                String allowedOrigins,
                String allowedOriginPatterns,
                String allowedHeaders,
                String exposedHeaders,
                String allowedMethods,
                Boolean allowCredentials,
                Long maxAge
        ) {

        }
    }
}

Мы сейчас сделали специально именно список конфигураций, чтобы добавить возможность настраивать CORS для различных ресурсов по-разному. Для демонстрации этого, добавим соответствующие значения в application.yml. При чем, для http://localhost:8080 и http://localhost:7778 будут отдельные конфигурации:

application.yml


  // .....
  spring:
    application:
      name: j-sso
    mvc:
      cors:
        configs:
          - pattern: /v3/api-docs
            allowed-origins: "http://localhost:7778"
            allowed-header: "*"
            exposed-headers: "*"
            allowed-methods: "*"
            allow-credentials: true
          - pattern: /**
            allowed-origins: "http://127.0.0.1:8080,http://localhost:8080"
            allowed-header: "*"
            exposed-headers: "*"
            allowed-methods: "*"
            allow-credentials: true

    // .....

Теперь изменим бин corsFilter, так чтобы он поддерживал вышеописанные настройки:

RootAppConfig.java


@Slf4j
@Configuration
@RequiredArgsConstructor
@EnableJpaRepositories(basePackages = "ru.dlabs.sas.example.jsso.dao.repository")
public class RootAppConfig {

    // .....

    @Bean
    public FilterRegistrationBean<CorsFilter> corsFilter() {
        log.debug("CREATE CORS FILTER");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        corsProperties.getConfigs().forEach(configProps -> {
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowCredentials(configProps.allowCredentials());
            config.addAllowedOrigin(configProps.allowedOrigins());
            config.addAllowedOriginPattern(configProps.allowedOriginPatterns());
            config.addAllowedHeader(configProps.allowedHeaders());
            config.addExposedHeader(configProps.exposedHeaders());
            config.addAllowedMethod(configProps.allowedMethods());
            source.registerCorsConfiguration(configProps.pattern(), config);
        });
        FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }

    // ....

}

Запустим и проверим как это работает совместно с j-swagger-ui.

Отображение спецификации j-sso в Swagger UI
Отображение спецификации j-sso в Swagger UI

Итак, все сработало как мы и хотели. Теперь давайте подключим j-service.

Для этого, добавим зависимость springdoc-openapi-starter-webmvc-api в его pom.xml. Да, именно ее, так как нам больше не нужен swagger-ui в модулях j-sso и j-service. Поэтому, у них обоих указываем зависимость springdoc-openapi-starter-webmvc-api вместо springdoc-openapi-starter-webmvc-ui.

j-service/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">

    // ....
    <dependencies>
        // ....
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
        </dependency>
    </dependencies>

    // ....

</project>

Чуть-чуть реструктуризируем директорию config в j-service. Все что относится к security вынесем в поддиректорию с одноименным названием. Добавим как и в j-sso класс RootAppConfig. Далее добавим CORS конфигурацию и бин OpenAPI openAPI() с информацией о сервисе. Все то же самое мы уже делали в j-sso, поэтому вы можете посмотреть RootAppConfig и application.yml в Github репозитории.

Вернемся к сервису j-swagger-ui и добавим новый сервис в параметр urls:

application.yml

// ....
springdoc:
  swagger-ui:
    enabled: true
    use-root-path: true
    doc-expansion: none
    disable-swagger-default-url: true
    operationsSorter: method
    tagsSorter: alpha
    try-it-out-enabled: true
    urls:
      - name: SSO Server
        url: http://localhost:7777/v3/api-docs
      - name: J Service
        url: http://localhost:9001/v3/api-docs

  // ....

Теперь запустим все три сервиса, откроем форму Swagger UI и видим, что в списке сервисов теперь их два и мы можем с легкостью переключаться между ними.

Отображение спецификации j-service в Swagger UI
Отображение спецификации j-service в Swagger UI

Итак, вроде бы теперь вообще все отлично и удобно. Но согласитесь, удобно было бы выводить информацию о времени и версии сборки из maven, а не руками менять это в java коде. Тем более в pom.xml есть дескрипторы <name> и <description>, которые предназначены для указания наименования модуля и его описания. Было бы удобно все это использовать для конфигурирования бина openAPI.

Оказывается в Spring Boot есть возможность указать в файле META-INF/build-info.properties информацию о сборке и потом использовать ее при помощи бина BuildProperties buildProperties. То есть мы можем при сборке создать файл build-info.properties и поместить туда информацию из pom.xml. Осталось решить, как это сделать. И тут разработчики Spring Boot уже за нас все придумали. В maven-плагине spring-boot-maven-plugin, есть возможность настройки задачи build-info, она как раз занимается созданием данного файла.

j-sso/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-authorization-server-example</artifactId>
        <groupId>ru.dlabs</groupId>
        <version>0.0.1</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>j-sso</artifactId>
    <name>SSO Server</name>
    <description>Единый сервис аутентификации пользователей</description>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <build-date>${maven.build.timestamp}</build-date>
        <maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
    </properties>

    // ....

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <finalName>${project.name}</finalName>
                </configuration>
                <executions>
                    <!-- Подключаем задачу создания build-info.properties файла-->
                    <execution>
                        <goals>
                            <goal>build-info</goal>
                        </goals>
                        <configuration>
                            <!-- Указываем дополнительные параметры: описание и время сборки-->
                            <additionalProperties>
                                <description>${project.description}</description>
                                <build-date>${build-date}</build-date>
                            </additionalProperties>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            // ....
        </plugins>
    </build>

</project>

После сборки, в директории j-sso/target/classes/META-INF можно найти файл build-info.properties со следующим содержимым:

build-info.properties

build.artifact=j-sso
build.build-date=2023-07-01 16\:57
build.description=Единый сервис аутентификации пользователей
build.group=ru.dlabs
build.name=SSO Server
build.time=2023-07-01T16\:57\:57.756Z
build.version=0.0.1

Теперь достаточно внедрить бин buildProperties в наш RootAppConfig и чуть изменить бин openAPI.

RootAppConfig.java


@Slf4j
@Configuration
@RequiredArgsConstructor
@EnableJpaRepositories(basePackages = "ru.dlabs.sas.example.jsso.dao.repository")
public class RootAppConfig {

    private final AppProperties.CorsProperties corsProperties;
    private final BuildProperties buildProperties;

    // .....

    @Bean
    public OpenAPI openAPI() {
        String buildDate = buildProperties.get("build-date");
        String buildInfo = "<h4>Build date: " + buildDate + "</h4>"
                + "<br>" + buildProperties.get("description");
        return new OpenAPI()
                .info(new Info()
                        .title(buildProperties.getName())
                        .description(buildInfo)
                        .version(buildProperties.getVersion())
                );
    }
}

Аналогично, сделаем и в j-service. После чего, пересоберем проект и запустим все три наших сервиса. Результаты представлены на скриншотах ниже.

Страница документации j-service
Страница документации j-service
Страница документации j-sso
Страница документации j-sso

Вроде бы, все теперь просто прекрасно, но осталась последняя не маловажная деталь. Нам необходимо донастроить Swagger UI таким образом, чтобы была возможность тестирования наших endpoint-ов. Она конечно и сейчас есть, но нет возможности подставить заголовок Authorization с токеном доступа. Для этого необходимо объявить бин с типом OperationCustomizer и в нем уже настроить добавление дополнительного параметра к каждой спецификации endpoint-а. Также, сразу добавим возможность включения/отключения этого заголовка, по средствам добавления параметра springdoc.auth.auth-header-enabled и аннотации @ConditionalOnProperty.

RootAppConfig.java


@Slf4j
@Configuration
@RequiredArgsConstructor
public class RootAppConfig {

    // ....

    @Bean
    @ConditionalOnProperty(value = "springdoc.auth.auth-header-enabled", havingValue = "true")
    public OperationCustomizer customGlobalHeaders() {
        return (Operation operation, HandlerMethod handlerMethod) -> {
            Parameter authorizationHeader = new Parameter()
                    .in(ParameterIn.HEADER.toString())
                    .schema(new StringSchema())
                    .name("Authorization")
                    .description("Authorization Header (Bearer or Basic)")
                    .required(false);
            operation.addParametersItem(authorizationHeader);
            return operation;
        };
    }
}

Запустим и посмотрим что получилось. После запуска, мы можем обнаружить следующую ошибку.

Тестируем работу endpoint-а /test
Тестируем работу endpoint-а /test

Ну конечно, мы же разрешили для swagger только получать спецификацию по пути /v3/api-docs, а чтобы со страницы swagger можно было делать запросы придется разрешить все endpoint-ы, по средствам указания следующего шаблона /** в CORS конфигурации. Сделав это запустим и посмотрим как оно работает.

Тестируем работу endpoint-а /test
Тестируем работу endpoint-а /test

Это конечно решило проблему с CORS, но почему у нас так и не отправляется заголовок Authorization, хотя он указан? Оказывается, swagger не разрешает в явную указывать заголовки: Content-Type, Accept, Authorization. Информацию об этом можно найти в этой документации. Но стоит отметить, что сам механизм работает, и при помощи бина OperationCustomizer customGlobalHeaders() можно установить любые глобальные параметры за исключением вышеописанных заголовков.

Так как теперь решить нашу задачу? Обратимся к документации Springdoc и посмотрим, что же там есть для авторизации. Оказывается, для решения нашей проблемы есть другой путь. Для его реализации необходимо изменить наш бин openAPI следующим образом:

RootAppConfig.java


@Slf4j
@Configuration
@RequiredArgsConstructor
public class RootAppConfig {

    @Value("${springdoc.auth.auth-header-enabled:false}")
    private Boolean authHeaderEnabled;

    private final AppProperties.CorsProperties corsProperties;
    private final BuildProperties buildProperties;

    // ...

    @Bean
    public OpenAPI openAPI() {
        String buildDate = buildProperties.get("build-date");
        String buildInfo = "<h4>Build date: " + buildDate + "</h4>"
                + "<br>" + buildProperties.get("description");

        Components components = new Components();
        List<SecurityRequirement> securityRequirements = new ArrayList<>();

        // добавляем возможность указывать Authorization header
        if (authHeaderEnabled) {
            String securitySchemeName = "Authorization header (Bearer)";
            components.addSecuritySchemes(securitySchemeName,
                    new SecurityScheme()
                            .type(SecurityScheme.Type.HTTP)
                            .scheme("bearer")
            );
            securityRequirements.add(new SecurityRequirement().addList(securitySchemeName));
        }

        return new OpenAPI()
                .components(components)
                .security(securityRequirements)
                .info(new Info()
                        .title(buildProperties.getName())
                        .description(buildInfo)
                        .version(buildProperties.getVersion())
                );
    }
}

Теперь запустим и увидим, что у нас появилась кнопка Authorize. При нажатии на нее у нас откроется модальное окно для ввода Bearer токена.

Модальное окно авторизации Swagger UI
Модальное окно авторизации Swagger UI

Получим токен при помощи нашего test-client и проверим как работает весь настроенный нами механизм.

Проверка работы функции try it out с указанием Bearer токена
Проверка работы функции try it out с указанием Bearer токена

Скриншот выше демонстрирует успешную работу Authorization заголовка.

Ну на этот раз все просто прекрасно и жизнь удалась, но согласитесь, что это не совсем удобно, получать токен при помощи стороннего ПО, а потом добавлять его сюда. Хочется все настроить в одном месте, так чтобы по кнопке Authorize можно было пройти полноценную аутентификацию и получить access токен. Копавшись в документации Swagger и Springdoc, я увидел, что и это возможно реализовать.

Для того чтобы добавить механизмы авторизации по протоколу OAuth 2 на форму Swagger UI, нам необходимо опять изменить наш бин openAPI. В него мы добавим дополнительные объекты SecurityScheme с типом SecurityScheme.Type.OAUTH2. Да, мы сразу добавим несколько типов авторизации:

  1. Authorization code flow

  2. Client Credentials Flow

Ниже представлен обновленный бин openAPI:

RootAppConfig.java


@Slf4j
@Configuration
@RequiredArgsConstructor
public class RootAppConfig {

    private final AppProperties.CorsProperties corsProperties;
    private final AppProperties.SwaggerProperties swaggerProperties;
    private final BuildProperties buildProperties;

    // ....

    @Bean
    public OpenAPI openAPI() {
        String buildDate = buildProperties.get("build-date");
        String buildInfo = "<h4>Build date: " + buildDate + "</h4>"
                + "<br>" + buildProperties.get("description");

        Components components = new Components();
        List<SecurityRequirement> securityRequirements = new ArrayList<>();

        // добавляем возможность указывать Authorization header
        if (swaggerProperties.getAuthTypes().authHeaderEnabled()) {
            String securitySchemeName = "Authorization header";
            components.addSecuritySchemes(securitySchemeName,
                    new SecurityScheme()
                            .type(SecurityScheme.Type.HTTP)
                            .scheme("bearer")
            );
            securityRequirements.add(new SecurityRequirement().addList(securitySchemeName));
        }

        // Добавим возможность OAuth2 Authorization code flow
        if (swaggerProperties.getAuthTypes().authorizationCodeEnabled()) {
            String securitySchemeName = "Authorization code flow";
            components.addSecuritySchemes(securitySchemeName, new SecurityScheme()
                    .type(SecurityScheme.Type.OAUTH2)
                    .flows(new OAuthFlows().authorizationCode(
                            new OAuthFlow()
                                    .tokenUrl(swaggerProperties.getAuthOauth().tokenUrl())
                                    .authorizationUrl(swaggerProperties.getAuthOauth().authorizationUrl())
                                    .refreshUrl(swaggerProperties.getAuthOauth().refreshUrl())
                    )));
            securityRequirements.add(new SecurityRequirement().addList(securitySchemeName));
        }

        // Добавим возможность OAuth2 Client credentials flow
        if (swaggerProperties.getAuthTypes().clientCredentialsEnabled()) {
            String securitySchemeName = "Client credentials flow";
            components.addSecuritySchemes(securitySchemeName, new SecurityScheme()
                    .type(SecurityScheme.Type.OAUTH2)
                    .flows(new OAuthFlows().clientCredentials(
                            new OAuthFlow().tokenUrl(swaggerProperties.getAuthOauth().tokenUrl())
                    )));
            securityRequirements.add(new SecurityRequirement().addList(securitySchemeName));
        }

        return new OpenAPI()
                .components(components)
                .security(securityRequirements)
                .info(new Info()
                        .title(buildProperties.getName())
                        .description(buildInfo)
                        .version(buildProperties.getVersion())
                );
    }
}

Изменения достаточно простые. Просто добавляем в компоненты новые security схемы с типом SecurityScheme.Type.OAUTH2 и настроенным объектом OAuthFlows.

Как вы можете заметить, мы создали класс AppProperties.SwaggerProperties по аналогии с AppProperties.CorsProperties, чтобы все настройки нашего Swagger были в одном месте. Реализация этого класса очень простая и вы можете ее посмотреть в репозитории.

Ниже представлен обновленный, в части настроек swagger, application.yml сервисов j-service и j-sso.

application.yml

// .....

springdoc:
  auth-types:
    auth-header-enabled: true
    client-credentials-enabled: true
    authorization-code-enabled: true
  auth-oauth:
    token-url: http://localhost:7777/oauth2/token
    authorization_url: http://localhost:7777/oauth2/authorize
    refresh-url: http://localhost:7777/oauth2/token
  swagger-ui:
    enabled: false
  api-docs:
    enabled: true
    path: /v3/api-docs

  // .....

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

Модальное окно авторизации Swagger UI
Модальное акно авториазции Swagger UI

В секции Authorization code flow введем client_id и client_secret от нашего test-client (а именно test-client/test-client). При нажатии на кнопку Authorize нас перенаправит на страницу логина j-sso. Пройдем аутентификацию и получим очередную ошибку.

Stacktrace ошибки авторизации
Stacktrace ошибки авторизации

Данная ошибка означает, что наш клиент не поддерживает то значение параметра redirect_uri, которое мы указали при запросе /oauth2/authorize. Кстати, а какой мы redirect_uri указали, и возможно ли его поменять? Обратимся к документации springdoc и найдем там параметр springdoc.swagger-ui.oauth2RedirectUrl. В значении по умолчанию там указан путь /swagger-ui/oauth2-redirect.html. Через этот параметр мы можем его изменить. Но менять мы его не будем, давайте просто пока в таблице БД sso.system_oauth2_clients руками добавим значение http://localhost:7778/swagger-ui/oauth2-redirect.html в колонке redirect_uris и проверим.

И снова мы получаем ошибку, но другую.

Stacktrace ошибки авторизации
Stacktrace ошибки авторизации

Данная ошибка говорит о том, что метод аутентификации не поддерживается. Давайте заглянем в табличку sso.system_oauth2_clients в колонку client_authentication_methods. Сейчас там стоит значение client_secret_basic. Это означает, что параметр client_secret передается только в заголовке Authorization с типом Basic. Давайте теперь посмотрим как swagger строит запросы. Для этого в браузере откроем консоль разработчика и посмотрим на payload запроса к j-sso.

Payload запроса к j-sso
Payload запроса к j-sso

Как видно, swagger все параметры передает через тело запроса POST. Я искал, как можно это изменить и заставить swagger передавать Authorization заголовок, но не нашел решения. Поэтому, пойдем другим путем и добавим возможность передавать client_secret в form-data. Для этого опять перейдем в табличку sso.system_oauth2_clients и добавим в колонке client_authentication_methods значение client_secret_post. Проверим как это будет работать.

Успешное выполнение запроса на endpoint /test
Успешное выполнение запроса на endpoint /test

На скриншоте выше показано выполнение запроса /test к сервису j-service после прохождения авторизации. В строке запроса можно видеть что Authorization заголовок передается и также можно наблюдать 200-ый код ответа.

Итак, мы полностью настроили работу swagger, но остался маленький штришок. Сейчас у нас везде используется в качестве клиента test-client. Давайте для swagger добавим отдельный клиент с его собственными настройками. Для этого, перейдем в файл j-sso/database/release-1.0.0/data/system-oauth2-clients-data.sql и добавим новый changeSet.

system-oauth2-clients-data.sql

--liquibase formatted sql

// ......

--changeSet daivanov:system-oauth2-clients-data-02
-- client_secret = swagger-client
INSERT INTO sso.system_oauth2_clients(client_id, client_secret,
                                      client_secret_expires_at,
                                      client_name, client_authentication_methods,
                                      authorization_grant_types, redirect_uris,
                                      scopes, client_settings, token_settings)
VALUES ('swagger-client', '$2a$10$G2AzJ0wujkRRbyU0hNWoNewpAfSPhg.cmo7HMT8cX9Xe6vdwmTMTW',
        to_timestamp('2072-01-01', 'YYYY-MM-DD'), 'Клиент для Swagger UI',
        'client_secret_post', 'authorization_code,refresh_token,client_credentials',
        'http://localhost:7778/swagger-ui/oauth2-redirect.html', 'read.scope,write.scope', null, null);

Таким образом мы добавили новый клиент для нашего swagger ui. На этом настройка Swagger заканчивается, теперь мы его можем использовать как полноценный инструмент вместо Postman.

Исходники данного раздела смотрите здесь.

Раздел 3.3: Настал час интерфейса SSO

Итак, настало время поговорить о кастомизации интерфейса нашего j-sso. В техническом требовании мы указывали, что необходимо использовать VueJS. Вот с него и начнем.

Во-первых, нам необходимо построить само VueJS приложение. Его мы расположим в директории j-sso/client. Создадим пока что простейшее приложение по средствам vue-cli (в конфигурации я указал использование vue 2, scss, vue-router и vuex). После этого попробуем поместить его вместо текущей формы логина j-sso, которая доступна по умолчанию от Spring Security.

Итак, после того как vue-cli создал нам приложение у нас в package.json файле доступны две задачи:

  1. serve - запустить используя dev server

  2. build - собрать приложение

Очевидно, что из этого нас сейчас интересует именно сборка, так как именно собранные файлы нам необходимо отдавать браузеру. Давайте более детально посмотрим что это за файлы:

Структура папки dist
Структура папки dist

После выполнения команды npm run build мы можем наблюдать, что в корневой директории клиента создалась директория dist - это и есть собранные файлы нашего приложения. Она состоит из 3 частей:

  • index.html - это главный html файл, который будет загружаться первым.

  • директория js - здесь находиться файлы со всем JavaScript-ом, который строит наш UI.

  • директория css - здесь находятся все стили css которые у нас используются в приложении.

Как же они между собой связаны? Давайте заглянем в index.html, ведь именно там и скрыт ответ на этот вопрос.

index.html

<!doctype html>
<html lang="">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="icon" href="/favicon.ico">
    <title>client</title>
    <script defer="defer" src="/js/chunk-vendors.6bb41578.js"></script>
    <script defer="defer" src="/js/app.5c0e5e95.js"></script>
    <link href="/css/app.bc18c568.css" rel="stylesheet">
</head>
<body>
<noscript><strong>We're sorry but client doesn't work properly without JavaScript enabled. Please enable it to
    continue.</strong></noscript>
<div id="app"></div>
</body>
</html>

Как мы помним из базовых уроков по html, у него есть дескриптор <head>, который хранит некую дополнительную информацию для браузера: стили, скрипты, шрифты и т.д.

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

Внутри этого дескриптора нас интересуют три последние строчки. В данных строчках происходит подключение JavaScript-а и css стилей из тех директорий, что мы рассмотрели выше. То есть это означает, что нам достаточно по запросу из web-сервера (коим у нас является Embedded Tomcat) отдать этот html файл браузеру и далее он самостоятельно подгрузит необходимые скрипты и стили. После чего наше frontend приложение заработает. А это в свою очередь означает что нам необходимо решить три маленькие задачи:

  1. Создать в нашем j-sso контроллер и endpoint, который будет возвращать index.html

  2. Необходимо, чтобы наш j-sso умел возвращать ресурсы (js, css, images и т.д.) по соответствующим запросам из index.html

  3. Скорее всего придется чуть-чуть видоизменить запросы до js и css, поэтому необходимо найти возможность кастомизации данных путей при сборке frontend приложения

Решаем первую задачу

Для ее решения нам необходимо вспомнить про Spring MVC и посмотреть как при помощи данного проекта реализовать нашу задачу. Оказывается, все очень просто. Достаточно создать простейший контроллер, с методом, который будет возвращать имя html файла. Данный html файл, в свою очередь, должен быть расположен в ресурсах приложения, в директории templates. Но, все это не будет работать без добавления зависимости spring-boot-starter-thymeleaf. Поэтому вначале добавляем зависимость в j-sso модуль.

j-sso/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    // ....

    <dependencies>

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

    </dependencies>

    // ....
</project>

Далее, создадим контроллер и поместим наш index.html из директории client/dist в директорию resources/templates. Не забудем выключить данный url из security конфигурации (чтобы не мешало экспериментам).

UIController.java


@Controller
public class UIController {

    @GetMapping("/ui-test")
    public String index() {
        return "index";
    }
}

Соберем, запустим j-sso и перейдем на URL http://localhost:7777/ui-test. Мы конечно увидим просто белую страницу, но нас сейчас не это интересует. Нас интересует следующее: подгрузился ли наш index.html файл? Для этого откроем консоль разработчика в браузере и посмотрим на ответ GET запроса /ui-test. Как видно из скриншота ниже, наш index.html успешно подгрузился. Соответственно, теперь нам нужно решить вторую задачу.

Загрузка файла index.html в браузере
Загрузка файла index.html в браузере

Решаем вторую задачу

Нам необходимо заставить подгружаться js и css файлы. По своей сути они являются статическими ресурсами, как например картинки. Соответственно, давайте просто решим задачу обслуживания статических ресурсов для Embedded Tomcat, который вшит в наше приложение j-sso.

Из данного руководства Spring мы можем найти, что достаточно поместить файлы в одну из следующих директорий и они будут доступны по соответствующим путям:

  • /META-INF/resources/

  • /resources/

  • /static/

  • /public/

Добавим все наши остальные файлы из директории client/dist в директорию src/main/resources/static. Отключим механизмы Security для следующих шаблонов: /favicon.ico, /js/**, /css/**.

Так как у нас стало достаточно много шаблонов запросов, которые необходимо вынести из под security, а менять сразу в двух конфигурационных классах (SecurityConfig и AuthorizationServerConfig) не особо удобно, то в SecurityConfig создадим для этого константу, а в AuthorizationServerConfig ее используем:

SecurityConfig.java


public class SecurityConfig {

    public static final String[] PERMIT_ALL_PATTERNS = {
            "/v3/api-docs",
            "/ui-test",
            "/favicon.ico",
            "/js/**",
            "/css/**"
    };

    // ....

    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        SocialConfigurer socialConfigurer = new SocialConfigurer()
                .oAuth2UserService(customOAuth2UserService);
        http.apply(socialConfigurer);

        http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(userDetailService)
                .passwordEncoder(passwordEncoder);

        http.authorizeHttpRequests(authorize ->
                authorize
                        // endpoint-ы которые вынесем из под security
                        .requestMatchers(PERMIT_ALL_PATTERNS).permitAll()
                        .anyRequest().authenticated()
        );
        return http.formLogin(withDefaults()).build();
    }
}

Теперь пересоберем проект и запустим. Далее перейдем на URL http://localhost:7777/ui-test.

Получение VueJS приложения через Embedded Tomcat
Получение VueJS приложения через Embedded Tomcat

На скриншоте выше мы можем видеть, что все файлы корректно загрузились и наше frontend приложение заработало.

Но это еще не все. Мне не нравиться, что все статические файлы имеют URL от корня приложения, это в дальнейшем будет трудно контролировать. А также есть вероятность, что оно создаст проблемы при вертикальном масштабировании приложения. Поэтому, хочу чтобы все ресурсы имели префикс /static/**.

В Spring Boot реализуется данное требование очень просто: достаточно добавить параметр spring.mvc.static-path-pattern. Кстати, в Spring Boot я также нашел параметр для явного указания директории со статическими ресурсами - spring.web.resources.static-locations.

application.yml

// ....
spring:
  application:
    name: j-sso
  mvc:
    static-path-pattern: /static/**
    cors:
      configs:
        - pattern: /**
          allowed-origins: "http://127.0.0.1:8080,http://localhost:8080,http://localhost:7778"
          allowed-headers: "*"
          exposed-headers: "*"
          allowed-methods: "*"
          allow-credentials: true
  web:
    resources:
      static-locations: classpath:static

  // ....

Не забудем выключить механизмы security на шаблоне /static/**, и перезапустим приложение.

Конечно, теперь front у нас сломан, потому что html пытается загрузить js и css по старым путям (от корня), а js и css теперь находятся по новому пути. Попробуем получить картинку favicon.ico по пути http://localhost:7777/static/favicon.ico.

Получение favicon.ico по новому пути
Получение favicon.ico по новому пути

Как видно из скриншота выше все сработало. И теперь мы переходим к третьей задаче.

Решаем третью задачу

Для решения данной задачи нам необходимо опять окунуться в мир frontend приложений и вспомнить, что сборщиком приложения у нас выступает Webpack. В Webpack для решения нашей задачи существует параметр output.publicPath (ссылка на документацию). А как его можно указать в нашем vue приложении? Для этого существует vue.config.js. Заглянем в документацию vue-cli и посмотрим, что там сказано про параметр publicPath.

Базовый URL-адрес сборки вашего приложения, по которому оно будет опубликовано (именуемая как baseUrl до версии Vue CLI 3.3). Это аналог опции webpack output.publicPath, но Vue CLI также использует это значение в других целях, поэтому вы должны всегда использовать publicPath вместо изменения опции output.publicPath.

Соответственно, добавим данный параметр в vue.config.js и укажем там значение /static.

vue.config.js

const {defineConfig} = require('@vue/cli-service');

module.exports = defineConfig({
    transpileDependencies: true,
    publicPath: process.env.VUE_APP_NODE_ENV !== "development" ? "/static" : "",
});

Обратите внимание, мы не просто указали путь, а поставили тернарный оператор, который ставит данный путь если переменная окружения VUE_APP_NODE_ENV не равна development. Это сделано для того, чтобы можно было работать с vue приложением как обычно (запуская его на node сервере). Данную переменную необходимо указать в .env файлах. Соответственно, создадим 4 файла:

  • .env.development - параметры для среды разработки

  • .env.production - параметры для продуктивной среды

  • .env.testing - параметры для среды тестирования

  • .env.dev-java - параметры для среды разработки (для сборки и использования в java приложении)

В них укажем пока только одну переменную VUE_APP_NODE_ENV со значением для соответствующего файла. Теперь, мы можем получить значение данной переменной следующим образом process.env.VUE_APP_NODE_ENV. Также изменим список задач в package.json.

package.json

{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve --mode=development",
    "build": "vue-cli-service build --mode=development",
    "build-prod": "vue-cli-service build --mode=production",
    "build-test": "vue-cli-service build --mode=testing",
    "build-dev-java": "vue-cli-service build --mode=dev-java"
  }
  // .....
}

Соберем наше Vue.js приложение следующей командой npm run build-prod и посмотрим на полученный index.html.

index.html

<!doctype html>
<html lang="">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="icon" href="/static/favicon.ico">
    <title>client</title>
    <script defer="defer" src="/static/js/chunk-vendors.efbb0edb.js"></script>
    <script defer="defer" src="/static/js/app.41afe825.js"></script>
    <link href="/static/css/app.bc18c568.css" rel="stylesheet">
</head>
<body>
<noscript><strong>We're sorry but client doesn't work properly without JavaScript enabled. Please enable it to
    continue.</strong></noscript>
<div id="app"></div>
</body>
</html>

Как видно из примера выше, мы получили, что хотели. Также, при запуске через npm run serve приложение тоже будет корректно работать.

Поместим сборку frontend приложения в ресурсы нашего j-sso. После пересборки и запуска приложения, UI будет отображаться корректно.

Автоматизация сборки

Согласитесь, не особо удобно каждый раз удалять и заново копипастить файлы в ресурсы j-sso. Давайте попробуем автоматизировать это. Есть много разных путей реализации этого: создать bash скрипты, проводить сборку в Docker и т.д. Но я хочу показать вариант с использованием только maven плагинов. Для этого нам понадобиться два плагина:

  • com.github.eirslett:frontend-maven-plugin - он нужен для выполнения npm задач. Также с его помощью можно скачать и использовать нужную версию Node и NPM без дополнительной их установки. Документацию можно посмотреть здесь

  • org.apache.maven.plugins:maven-resources-plugin - данным плагином мы будем переносить файлы из client/dist в target. Документацию можно посмотреть здесь.

Итак, приступим. Подключим данные плагины и настроим следующие задачи:

Плагин frontend-maven-plugin:

  1. install node and npm - установка Node и NPM для сборки фронта

  2. npm install - выполнение задачи установки зависимостей для фронта

  3. npm build - сборка фронта

Плагин maven-resources-plugin:

  1. copy-to-templates - задача копирования index.html файла из client/dist в target/classes/templates

  2. copy-to-static - задача копирования остальных файлов из client/dist в target/classes/static

j-sso/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-authorization-server-example</artifactId>
        <groupId>ru.dlabs</groupId>
        <version>0.0.1</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>j-sso</artifactId>
    <name>SSO Server</name>
    <description>Единый сервис аутентификации пользователей</description>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <build-date>${maven.build.timestamp}</build-date>
        <maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
    </properties>

    // ......

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>com.github.eirslett</groupId>
                <artifactId>frontend-maven-plugin</artifactId>
                <executions>
                    <!-- Установка Node и NPM для сборки фронта-->
                    <execution>
                        <id>install node and npm</id>
                        <goals>
                            <goal>install-node-and-npm</goal>
                        </goals>
                        <phase>generate-resources</phase>
                    </execution>
                    <!-- Выполнение задачи установки зависимостей для фронта-->
                    <execution>
                        <id>npm install</id>
                        <goals>
                            <goal>npm</goal>
                        </goals>
                        <phase>generate-resources</phase>
                        <configuration>
                            <workingDirectory>client</workingDirectory>
                            <arguments>install</arguments>
                        </configuration>
                    </execution>
                    <!-- Сборка фронта-->
                    <execution>
                        <id>npm build</id>
                        <goals>
                            <goal>npm</goal>
                        </goals>
                        <phase>generate-resources</phase>
                        <configuration>
                            <workingDirectory>client</workingDirectory>
                            <arguments>run build-dev-java</arguments>
                        </configuration>
                    </execution>
                </executions>
                <configuration>
                    <nodeVersion>v16.20.1</nodeVersion>
                    <npmVersion>9.7.2</npmVersion>
                    <nodeDownloadRoot>https://nodejs.org/dist/</nodeDownloadRoot>
                    <npmDownloadRoot>https://registry.npmjs.org/npm/-/</npmDownloadRoot>
                    <installDirectory>.node</installDirectory>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <executions>
                    <!-- Копирование index.html файла из client/dist в target/classes/templates-->
                    <execution>
                        <id>copy-templates</id>
                        <phase>generate-resources</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${basedir}/target/classes/templates</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>${basedir}/client/dist</directory>
                                    <includes>
                                        <include>index.html</include>
                                    </includes>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                    <!-- Копирование js, css, images и т.д. из client/dist в target/classes/static-->
                    <execution>
                        <id>copy-static</id>
                        <phase>generate-resources</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${basedir}/target/classes/static</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>${basedir}/client/dist</directory>
                                    <includes>
                                        <include>favicon.ico</include>
                                        <include>js/**</include>
                                        <include>css/**</include>
                                    </includes>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

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

  1. Скачивать Node и npm каждый раз как будет собираться приложение не нужно. Достаточно один раз скачать.

  2. Устанавливать зависимости frontend приложения и производить его сборку, если оно не изменялось, бессмысленно каждый раз когда пересобирается java приложение

  3. У frontend приложения есть как минимум 3 профиля сборки: development, test и production. В Java приложении, тоже скорее всего понадобятся профили сборки. Нужно как-то синхронизировать их.

Итак, чтобы решить первую и вторую задачу, нам необходимо понять как можно включить/выключить конкретную задачу. Мы помним, что Maven запускает задачу (execution) только тогда, когда у него есть привязка к параметру build phase. То есть простыми словами, когда есть дескриптор <phase> и в нем установлено корректное значение. Поэтому, универсальным способом включить/выключить будет смена параметра phase у execution. Но как мы это можем реализовать?

И тут на сцену выходят профили сборки maven и <properties>. Создадим следующие properties:

  1. build-client.phrase - будет содержать значение phase для задачи сборки frontend приложения. Но сборка не возможна без установки Node и NPM, если до этого они не были установлены. Также, сборка не возможна, если не была выполнена задача установки зависимостей. Давайте объединим запуск установки Node и NPM, загрузки зависимостей и сборки под один параметр. Кстати, установка Node и NPM не будет выполняться, если они уже установленны. Здесь за нас поработали разработчики плагина.

  2. copy-client-build.phrase - будет содержать значение phase для задачи копирования сборки frontend приложения в target директорию.

  3. client-build-command.param - будет содержать наименование скрипта сборки frontend приложения (build-prod, build, build-test)

И далее, нам осталось создать профили сборки maven, которыми мы будем указывать, что необходимо включить, а что выключить. Поэтому, создадим следующие профили сборки:

  • dev - профиль сборки для среды разработки. По умолчанию активен

  • test - профиль сборки для среды тестирования

  • prod - профиль сборки для продуктива

  • client-build-and-copy - профиль сборки, при котором произойдет полная сборка и копирование файлов сборки фронта в ресурсы сборки java приложения

  • client-only-copy - профиль сборки, при котором будет выполнено только копирование файлов сборки фронта в ресурсы сборки java приложения

Ниже представлен файл pom.xml для j-sso.

j-sso/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-authorization-server-example</artifactId>
        <groupId>ru.dlabs</groupId>
        <version>0.0.1</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>j-sso</artifactId>
    <name>SSO Server</name>
    <description>Единый сервис аутентификации пользователей</description>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <build-date>${maven.build.timestamp}</build-date>
        <maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>

        <build-client.phrase>disabled</build-client.phrase>
        <copy-client-build.phrase>disabled</copy-client-build.phrase>
        <client-build-command.param>build-dev-java</client-build-command.param>
    </properties>

    // ......

    <profiles>
        <!-- Профиль для сборки и копирования frontend приложения-->
        <profile>
            <id>client-build-and-copy</id>
            <properties>
                <build-client.phrase>generate-resources</build-client.phrase>
                <copy-client-build.phrase>generate-resources</copy-client-build.phrase>
            </properties>
        </profile>
        <!-- Профиль для только копирования frontend приложения-->
        <profile>
            <id>client-only-copy</id>
            <properties>
                <build-client.phrase>non</build-client.phrase>
                <copy-client-build.phrase>generate-resources</copy-client-build.phrase>
            </properties>
        </profile>
        <!-- Стандартный профиль сборки для dev среды, по умолчанию активен-->
        <profile>
            <id>dev</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <client-build-command.param>build-dev-java</client-build-command.param>
            </properties>
        </profile>
        <!-- Стандартный профиль сборки для test среды-->
        <profile>
            <id>test</id>
            <properties>
                <client-build-command.param>build-test</client-build-command.param>
            </properties>
        </profile>
        <!-- Стандартный профиль сборки для production среды-->
        <profile>
            <id>prod</id>
            <properties>
                <client-build-command.param>build-prod</client-build-command.param>
            </properties>
        </profile>
    </profiles>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>com.github.eirslett</groupId>
                <artifactId>frontend-maven-plugin</artifactId>
                <executions>
                    <!-- Установка Node и NPM для сборки фронта-->
                    <execution>
                        <id>install node and npm</id>
                        <goals>
                            <goal>install-node-and-npm</goal>
                        </goals>
                        <phase>${build-client.phrase}</phase>
                    </execution>
                    <!-- Выполнение задачи установки зависимостей для фронта-->
                    <execution>
                        <id>npm install</id>
                        <goals>
                            <goal>npm</goal>
                        </goals>
                        <phase>${build-client.phrase}</phase>
                        <configuration>
                            <workingDirectory>client</workingDirectory>
                            <arguments>install</arguments>
                        </configuration>
                    </execution>
                    <!-- Сборка фронта-->
                    <execution>
                        <id>npm build</id>
                        <goals>
                            <goal>npm</goal>
                        </goals>
                        <phase>${build-client.phrase}</phase>
                        <configuration>
                            <workingDirectory>client</workingDirectory>
                            <arguments>run ${client-build-command.param}</arguments>
                        </configuration>
                    </execution>
                </executions>
                <configuration>
                    <nodeVersion>v16.20.1</nodeVersion>
                    <npmVersion>9.7.2</npmVersion>
                    <nodeDownloadRoot>https://nodejs.org/dist/</nodeDownloadRoot>
                    <npmDownloadRoot>https://registry.npmjs.org/npm/-/</npmDownloadRoot>
                    <installDirectory>.node</installDirectory>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <executions>
                    <!-- Копирование index.html файла из client/dist в target/classes/templates-->
                    <execution>
                        <id>copy-templates</id>
                        <phase>${copy-client-build.phrase}</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${basedir}/target/classes/templates</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>${basedir}/client/dist</directory>
                                    <includes>
                                        <include>index.html</include>
                                    </includes>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                    <!-- Копирование js, css, images и т.д. из client/dist в target/classes/static-->
                    <execution>
                        <id>copy-static</id>
                        <phase>${copy-client-build.phrase}</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${basedir}/target/classes/static</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>${basedir}/client/dist</directory>
                                    <includes>
                                        <include>favicon.ico</include>
                                        <include>js/**</include>
                                        <include>css/**</include>
                                    </includes>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Теперь, выполнив команду clean install -DskipTests -P dev,client-build-and-copy мы соберем как frontend приложение, так и backend приложение нашего SSO. А поменяв профиль clean install -DskipTests -P dev,client-only-copy мы скопируем файлы сборки frontend приложения в target директорию и соберем java приложение. При этом, из директории resources можно удалить те файлы сборки, которые мы добавляли вручную.

Если вы используете IntelliJ IDEA, то не забудьте изменить конфигурацию запуска, указав корректную задачу сборки. Иначе, не будет работать фронт.

Настройки запуска j-sso в IntelliJ IDEA
Настройки запуска j-sso в IntelliJ IDEA

Создание собственной формы логина

Раз мы разобрались, как Vue приложение использовать через Spring Boot приложение. Давайте заменим форму логина, которую нам предоставляет Spring Security на наше Vue приложение. Для этого первым шагом мы должны кастомизировать DSL метод formLogin() в SecurityConfig и в SocialConfigurer. При кастомизации мы укажем loginPage и success/failure Handlers. Ниже представлен обновленный SecurityConfig. successHandler и failureHandler пока создадим те же, что используются по умолчанию, в дальнейшем они нам пригодятся.

SecurityConfig.java

public class SecurityConfig {

    public static final String LOGIN_PAGE = "/login";
    public static final String[] PERMIT_ALL_PATTERNS = {
            LOGIN_PAGE,
            "/static/**"
    };

    private final CustomOAuth2UserService customOAuth2UserService;
    private final CustomUserDetailsService userDetailService;
    private final PasswordEncoder passwordEncoder;
    private final AuthenticationSuccessHandler successHandler = new SimpleUrlAuthenticationSuccessHandler();
    private final AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        SocialConfigurer socialConfigurer = new SocialConfigurer()
                .oAuth2UserService(customOAuth2UserService)
                .successHandler(successHandler)
                .failureHandler(failureHandler)
                .formLogin(LOGIN_PAGE);
        http.apply(socialConfigurer);

        http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(userDetailService)
                .passwordEncoder(passwordEncoder);

        http.authorizeHttpRequests(authorize ->
                authorize
                        .requestMatchers(PERMIT_ALL_PATTERNS).permitAll()
                        .anyRequest().authenticated()
        );
        return http.formLogin(configurer -> {
            // кастомизируем форму логина
            configurer.loginPage(LOGIN_PAGE)
                    .successHandler(successHandler)
                    .failureHandler(failureHandler);
        }).build();
    }
}

Вторым шагом, кастомизируем UIController. Это необходимо чтобы на запрос /login была возвращена html страница нашего frontend приложения.

UIController.java


@Controller
public class UIController {

    @GetMapping("/login")
    public String index() {
        return "index";
    }
}

В результате, когда мы пересоберем и запустим наш проект, то увидим на странице логина наше frontend приложение.

Отображение vue приложения на запрос /login
Отображение vue приложения на запрос /login

Теперь, когда все основные связующие механизмы мы создали, осталось создать frontend приложение, сделать нормальную форму логина и дальше смело проектировать новые функциональности. Ниже показан скриншот новой рабочей формы логина. Сейчас мы не будем погружаться в детали построения frontend приложения, сделаем это в следующей статье. Единственное, давайте разберем логику взаимодействия с API j-sso.

Ниже представлен основной сервис для реализации этого взаимодействия - login-service.js

login-service.js

import axios from 'axios';

export class LoginAPI {
    __LOGIN_URL = "/login";
    __OAUTH_AUTHORIZATION_URL = "/oauth2/authorization/";
    __LOCATION_HEADER = process.env.VUE_APP_SSO_LOCATION_HEADER;

    /**
     * Вход черед логин/пароль.
     * При успешной аутентификации получает в заголовках ответа специальный
     * заголовок {@see process.env.VUE_APP_SSO_LOCATION_HEADER} в котором содержится URL для дальнейшего перехода
     * @param username - логин
     * @param password - пароль
     */
    login(username, password) {
        let formData = new FormData();
        formData.append("username", username);
        formData.append("password", password);

        return axios.post(this.__LOGIN_URL, formData).then(result => {
            // проверяем есть ли спец. заголовок
            if (result.headers.has(this.__LOCATION_HEADER)) {

                // переходим на указанный в заголовке адрес
                window.location = result.headers.get(this.__LOCATION_HEADER);
            }
        });
    }

    /**
     * Метод запуска процесса авторизации через Yandex, Github или Google
     * @param providerName - одно из следующих значений: google, github, yandex
     */
    loginWith(providerName) {
        window.location = this.__getOAuthAuthorizationUrl(providerName);
    }

    __getOAuthAuthorizationUrl(providerName) {
        return this.__OAUTH_AUTHORIZATION_URL + providerName;
    }
}


export default new LoginAPI();

Вроде бы все очевидно, но есть один нюанс. Он заключается в обработке успешной аутентификации по средствам логина и пароля. При чем, он затрагивает не только frontend приложение, но и backend тоже. Нюанс заключается в том, что axios, в силу использования XMLHttpRequest, не позволяет вручную обрабатывать редиректы (параметр maxRedirects - работает только для приложений node.js). А как мы помним, Spring Security в j-sso при успешной авторизации перенаправляет нас на корень приложения. При этом, в силу особенностей реализации механизма Social Login, данное перенаправление не создает проблем. Соответственно, дабы не придумывать кучу "велосипедов", я решил поддержать реализацию с перенаправлением.

Смысл ее заключается в том, что при аутентификации через форму мы редирект будем выполнять на frontend приложении. А URL для этого перенаправления мы будем получать через специальный заголовок HTTP ответа. Поэтому, для реализации такого подхода необходимо при настройке Spring Security указать кастомизированный AuthenticationSuccessHandler, что я и сделал.

Ниже представлен обновленный SecurityConfig и новый обработчик CustomAuthenticationSuccessHandler.

SecurityConfig.java


@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {

    public static final String LOGIN_PAGE = "/login";
    public static final String[] PERMIT_ALL_PATTERNS = {
            LOGIN_PAGE,
            "/static/**"
    };

    private final CustomOAuth2UserService customOAuth2UserService;
    private final CustomUserDetailsService userDetailService;
    private final PasswordEncoder passwordEncoder;
    private final AuthorizationServerProperties authorizationServerProperties;

    // handlers
    private AuthenticationSuccessHandler oAuth2successHandler;
    private AuthenticationSuccessHandler loginRequestSuccessHandler;
    private AuthenticationFailureHandler failureHandler;

    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        SocialConfigurer socialConfigurer = new SocialConfigurer()
                .oAuth2UserService(customOAuth2UserService)
                .successHandler(oAuth2successHandler)
                .failureHandler(failureHandler)
                .formLogin(LOGIN_PAGE);
        http.apply(socialConfigurer);
        http.csrf(AbstractHttpConfigurer::disable);

        http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(userDetailService)
                .passwordEncoder(passwordEncoder);

        http.authorizeHttpRequests(authorize ->
                authorize
                        .requestMatchers(PERMIT_ALL_PATTERNS).permitAll()
                        .anyRequest().authenticated()
        );
        return http.formLogin(configurer -> {
            configurer.loginPage(LOGIN_PAGE)
                    .loginProcessingUrl(LOGIN_PAGE)
                    .successHandler(loginRequestSuccessHandler)
                    .failureHandler(failureHandler);
        }).build();
    }

    @PostConstruct
    private void initializeHandlers() {
        // создаем кастомный AuthenticationSuccessHandler для формы логина
        this.loginRequestSuccessHandler = new CustomAuthenticationSuccessHandler(
                authorizationServerProperties.getAuthenticationSuccessUrl(),
                authorizationServerProperties.getCustomHandlerHeaderName()
        );

        // указываем стандартный AuthenticationSuccessHandler для OAuth2 Client
        this.oAuth2successHandler = new SimpleUrlAuthenticationSuccessHandler(
                authorizationServerProperties.getAuthenticationSuccessUrl()
        );
        this.failureHandler = new SimpleUrlAuthenticationFailureHandler();
    }
}

CustomAuthenticationSuccessHandler.java


@Slf4j
@RequiredArgsConstructor
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private final String locationUrl;
    private final String headerName;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("Authentication success");
        response.setHeader(headerName, locationUrl);
    }
}

Таким образом после пересборки и запуска j-sso, мы получаем следующий результат:

Новая страница логина j-sso
Новая страница логина j-sso

На этом все, нами выставленные, технические требования, мы выполнили.

Исходники данного раздела смотрите здесь.

Резюме

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

  • Использование непрозрачных токенов

  • Использование последних версий Spring Boot и Spring Authorization Server

  • Использование Java 17

  • Использование SPA Vue.JS приложения в качестве фронта SSO

  • Использование Redis в качестве кэш хранилища (хранение токенов и т.д.)

  • Использование PostgreSQL в качестве основного хранилища

  • Подключить Swagger и настроить там авторизацию

В следующей статье мы детально разберем построение frontend приложения модуля j-sso. Поговорим о его стеке используемых библиотек. И приступим к реализации оставшихся функциональных требований:

  • Регистрация пользователей через отдельную форму регистрации на SSO

  • Реализация функции "Забыли пароль" (это новое требование)

  • Возможность управления выданными токенами (отзыв токена, просмотр активных сессий и т.д.)

Полезные ссылки

  1. Исходники смотрите здесь

  2. Redis image. Docker Hub

  3. Документация Spring Data Redis

  4. Документация Spring Session

  5. Параметры настройки Spring Session

  6. Параметры настройки Spring Data Redis

  7. OpenAPI Specification

  8. Документация Springdoc

  9. Документация Swagger OpenAPI

  10. Документация Spring MVC (Serving Web Content with Spring MVC)

  11. Tutorial Spring Boot - Thymeleaf

  12. Tutorial обслуживание статичных ресурсов Spring Boot

  13. Репозиторий/документация frontend-maven-plugin

  14. Документация maven-resources-plugin

  15. Создание проекта с использованием Vue Cli

  16. Конфигурация Vue Cli

  17. Документация Webpack

Источник: https://habr.com/ru/articles/748584/


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

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

В июне 2021 года вышла статья “A Survey of Transformers” - обзор различных нововведений, сделанных с применением архитектуры “трансформер” после ее появления в материале “Attention is all you need”.Эт...
Эта текст покрывает ответы на некоторые совсем базовые вопросы и вместе с тем сразу погружает в проблематику получения ответа на вопрос: "как работать лучше? однопоточно, многопоточно или многопоточно...
В третьей части ищем ошибку при удалении задачи из списка дел, и попутно путешествуем во времени разбираемся со стеком вызовов в отладчике.
Технологические гиганты при помощи денег инвестиционных фондов контролируют всё большую часть новых разработчиков и продуктов, перекрывая тем самым путь для новых програм...
В данном техническом обзоре мы детально познакомимся с продуктом Instana – инструментом для автоматического мониторинга производительности микросервисной инфраструктуры, ...