DevGang
Авторизоваться

Липкие сессии с Apache APISIX – демонстрация

Концепция липких сессий (sticky sessions): вы направляете запрос на определённый апстрим, потому что в этом узле есть контекстные данные, связанные с сессией. Однако при необходимости вы должны скопировать данные на другие апстримы, поскольку этот может выйти из строя. В этом посте мы собираемся проиллюстрировать это на примере.

Общая конструкция

Варианты конструкции безграничны. Я ограничусь знакомым стеком - JVM. Помимо того, что уже упоминалось в предыдущем посте, липкие сессии следует реализовывать только с репликацией сессий. Окончательная конструкция состоит из двух компонентов: экземпляра Apache APISIX с настроенными липкими сессиями и двух узлов JVM, на которых выполняется одно и то же приложение с репликацией сессий.

В приложении используется следующее:

Зависимость Описание
Spring Boot Облегчает использование библиотек Spring
Spring MVC Позволяет предлагать конечные точки HTTP
Thymeleaf Показывает технологию
Spring Session Предлагает абстракцию для репликации сессий
Hazelcast (встроенный) Осуществляет репликацию сессий
Spring Security Привязывает идентификатор к сеансу пользователя

Конструкция выглядит следующим образом:

app1 и app2 — это два экземпляра одного и того же приложения. Я не хотел перегружать диаграмму избыточными данными.

Основа приложения

Основой приложения является скопированный в сессию бин, который является оболочкой счетчика, значение которого может быть только увеличено:

@Component
@SessionScope
public class Counter implements Serializable {              //1

    private int value = 0;

    public int getValue() {
        return value;
    }

    public void incrementValue() {
        value++;
    }
}
  • Необходимо для осуществления сериализации Hazelcast

Мы можем использовать этот бин в контроллере:

@Controller
public class IndexController {

    private final Counter counter;

    public IndexController(Counter counter) {               //1
        this.counter = counter;
    }

    @GetMapping("/")
    public String index(Model model) {                      //2
        counter.incrementValue();
        model.addAttribute("counter", counter.getValue());
        return "index";
    }
}
  • Внедрите скопированный в сессии бин в контроллер синглтона с помощью Spring
  • Когда мы отправляем запрос GET к руту, мы увеличиваем значение счетчика и передаем его в модель.

Наконец, мы отображаем значение бина на странице Thymeleaf:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<body>
<div th:text="${counter}">3</div>

Конфигурирование Spring Session с помощью Hazelcast

Spring Session предлагает фильтр, который оборачивает исходный HttpServletRequest, чтобы переопределить метод getSession(). Этот метод возвращает конкретную реализацию Session, подкрепленную реализацией, настроенной с помощью Spring Session, в нашем случае с помощью Hazelcast.

Для настройки Spring Session с помощью Hazelcast нам потребуется всего несколько настроек.

Во-первых, аннотируйте класс приложения Spring Boot соответствующей аннотацией:

@SpringBootApplication
@EnableHazelcastHttpSession
public class SessionApplication {
    ...
}

Hazelcast также требует определенной конфигурации. Мы можем использовать XML, YAML или код. Поскольку это демоверсия, я могу выбрать всё, что захочу, поэтому давайте напишем код. Spring Boot требует либо объект Hazelcast, либо объект конфигурации. Последнего будет достаточно:

@Bean
public Config hazelcastConfig() {
  var config = new Config();
  var networkConfig = config.getNetworkConfig();
  networkConfig.setPort(0);                                                      //1
  networkConfig.getJoin().getAutoDetectionConfig().setEnabled(true);              //2
  var attributeConfig = new AttributeConfig()                                     //3
          .setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
          .setExtractorClassName(PrincipalNameExtractor.class.getName());
  config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME) //3
          .addAttributeConfig(attributeConfig)
          .addIndexConfig(new IndexConfig(
            IndexType.HASH,
            HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE
  ));
  var serializerConfig = new SerializerConfig();
  serializerConfig.setImplementation(new HazelcastSessionSerializer())            //3
                  .setTypeClass(MapSession.class);
  config.getSerializationConfig().addSerializerConfig(serializerConfig);
  return config;
  1. Выберите случайный порт, чтобы избежать несостыковки портов.
  2. Разрешите Hazelcast искать другие экземпляры и автоматически формировать кластер. Это будет необходимо при развертывании в соответствии с нашей конструкцией.
  3. Скопировано из документации Spring Session.

Конфигурирование Spring Security

Большинство примеров Spring Session так или иначе используют Spring Security, и, хотя это не является строго необходимым, это облегчает проектирование. Сначала я хочу объяснить, почему.

Можно рассматривать сессии как гигантскую хэш-таблицу. В обычных приложениях ключом является значение куки JSESSIONID, значение - другая хэш-таблица данных сессии. Однако JSESSIONID специфичен для узла. Приложение выдаст другой JSESSIONID, если использовать другой узел. Поскольку ключ отличается, нет возможности получить доступ к данным сессии, даже если данные сессии распределяются между узлами. Чтобы предотвратить эту потерю, нам нужно придумать другой ключ. Spring Security позволяет использовать принципал или имя пользователя в качестве ключа данных сессии.

Вот как я установил базовую конфигурацию Spring Security:

@Bean
public SecurityFilterChain securityFilterChain(UserDetailsService service, HttpSecurity http) throws Exception {
  return http.userDetailsService(service)                                    //1
             .authorizeHttpRequests(authorize -> authorize.requestMatchers(
                 PathRequest.toStaticResources().atCommonLocations())        //2
                            .permitAll()                                     //2
                            .anyRequest().authenticated()                    //3
             ).formLogin(form -> form.permitAll()
                                     .defaultSuccessUrl("/")                 //4
             ).build();
}
  1. 1. Служба сведений о пользователе в памяти по умолчанию не позволяет использовать пользовательские классы сведений о пользователе. Мне пришлось создать свой собственный.
  2. Необходимо разрешить всем доступ к статическим ресурсам в "общих" местах.
  3. Все остальные запросы должны быть аутентифицированы.
  4. Необходимо разрешить всем доступ к форме аутентификации.
  5. В случае успеха перенаправить на корень, который отображает вышеуказанный контроллер.

Испытание нашей конструкции

Помимо счётчика, я хочу отобразить два дополнительных элемента данных: имя хоста и вошедшего пользователя.

Для имени хоста я добавляю в контроллер следующий метод:

@ModelAttribute("hostname")
private String hostname() throws UnknownHostException {
    return InetAddress.getLocalHost().getHostName();
}

Отображение вошедшего в систему пользователя требует дополнительной зависимости:

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>

На странице все предельно просто:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">  <!--1-->
<body>
<td sec:authentication="principal.label">Me</td>                    <!--2-->
  • Добавьте пространство имен sec. Это не обязательно, но может помочь IDE помочь Вам.
  • Требуйте, чтобы базовая реализация UserDetail имела метод getLabel().

И последнее, но не менее важное: нам нужно настроить Apache APISIX с липкими сессиями, как мы могли видеть на прошлой неделе:

routes:
  - uri: /*
    upstream:
      nodes:
        "webapp1:8080": 1
        "webapp2:8080": 1
      type: chash
      hash_on: cookie
      key: cookie_JSESSIONID
#END

Вот дизайн, реализованный на Docker Compose:

services:
  apisix:
    image: apache/apisix:3.3.0-debian
    volumes:
      - ./apisix/config.yml:/usr/local/apisix/conf/config.yaml:ro
      - ./apisix/apisix.yml:/usr/local/apisix/conf/apisix.yaml:ro    #1
    ports:
      - "9080:9080"                                                  #2
    depends_on:
      - webapp1
      - webapp2
  webapp1:
    build: ./webapp
    hostname: webapp1                                                #3
  webapp2:
    build: ./webapp
    hostname: webapp2  
  1. Используйте предыдущий файл конфигурации
  2. Выставите шлюз API только для внешнего мира
  3. Установите имя хоста, чтобы отобразить его на странице.

Мы можем войти в систему, используя одну из двух заданных учётных записей. Я использую john, с паролем john и меткой John Doe. Обратите внимание, что Apache APISIX направляет меня на определённый узел и продолжит использовать этот же самый узел, если я обновлю.

Мы можем попробовать войти в систему с другой учетной записью (jane/jane) из личного окна и проверить, что счётчик начинается с 0.

Теперь начинается самое интересное. Давайте остановим узел, который должен хранить данные сессии, здесь это узел webapp2, и обновим страницу:

docker compose stop webapp2

В журналах мы можем увидеть интересные вещи. Apache APISIX больше не может найти webapp2, поэтому он перенаправляет запрос на другой известный ему апстрим, webapp1.

  1. Запрос по-прежнему аутентифицирован; он проходит через Spring Security.
  2. Фреймворк получает главного пользователя из запроса.
  3. Фреймворк запрашивает Spring Session.
  4. Фреймворк получает правильное значение счетчика, которое Hazelcast реплицировал с другого узла.

Единственным побочным эффектом является увеличение задержки из-за таймаута Apache APISIX. По умолчанию он составляет 5 секунд, но при необходимости вы можете настроить его на меньшее значение.

Когда мы снова запускаем webapp2, всё работает в соответствии с ожиданиями.

Заключение

В этом посте я описал возможную настройку для липких сессий с Apache APISIX и репликации с использованием экосистемы Spring и Hazelcast. В зависимости от выбранного вами стека и фреймворка возможно множество других вариантов.

Полный исходный код этой заметки можно найти на GitHub:

https://github.com/ajavageek/sticky-sessions-apisix

Источник:

Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу