ThunderOTP — архитектурный подход, реализованный с помощью облегченных облачных микросервисов с использованием Kotlin.

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

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

В этой статье я хотел бы объяснить архитектурный подход, применяемый для реализации решения без пароля, которое можно использовать как автономный механизм проверки подлинности или как часть более сложного решения MFA. Я подчеркну модель горизонтального масштабирования, применяемую с использованием Redis Cluster, и подход нативного образа с GraalVM для реализации легкого и эффективного уровня сервисов.



Что такое беспарольная аутентификация?

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

Пароли уже давно не в безопасности. Их трудно запомнить и легко потерять. Они также являются мишенью номер один для киберпреступников. Настолько, что 81% взломов связаны со слабыми или украденными паролями.

Важно четко различать различные методы, используемые для беспарольной аутентификации. Некоторые из них более безопасны, а некоторые обеспечивают лучший пользовательский интерфейс. Эта архитектура реализована с использованием одноразовых кодов (OTP). Они наиболее известны процессами многофакторной аутентификации, но одноразовые пароли или коды также могут использоваться в качестве автономного метода аутентификации.

Какие существуют типы беспарольной аутентификации?

Аутентификация без пароля может быть достигнута многими способами. Вот некоторые из них:

  • Биометрия: физические признаки, такие как сканирование отпечатков пальцев или сетчатки глаза, и поведенческие признаки, такие как набор текста и динамика сенсорного экрана, используются для однозначной идентификации человека.
  • Факторы владения: Аутентификация через то, что пользователь имеет или носит с собой. Например, код, сгенерированный приложением для аутентификации смартфона, одноразовые пароли, полученные через SMS или аппаратный токен.
  • Волшебные ссылки: пользователь вводит свой адрес электронной почты, и система отправляет ему электронное письмо. Электронное письмо содержит ссылку, которая предоставляет пользователю доступ при нажатии.

Как OTP работают в беспарольной аутентификации?

Одноразовые пароли (или OTP) представляют собой числовые коды, связанные со ссылкой. Эти коды отправляются пользователю, поэтому только сервер и пользователь могут знать этот код. Когда пользователь вводит код на платформе, ему предоставляется доступ и аутентификация.

Эти коды будут отправлены на телефон пользователя с помощью SMS, push-уведомлений или электронной почты.

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

MFA против аутентификации без пароля

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

Например, система MFA может использовать сканирование отпечатков пальцев в качестве основного фактора аутентификации, а SMS-одноразовые пароли — в качестве вторичного.

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

Преимущества беспарольной аутентификации

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

  • Улучшите взаимодействие с пользователем, устранив усталость от паролей и секретов и предоставив унифицированный доступ ко всем приложениям и службам.
  • Повысьте безопасность, устранив рискованные методы управления паролями и сократив количество случаев кражи учетных данных и выдачи себя за другое лицо.
  • Упростите ИТ-операции — избавьтесь от необходимости выдавать, защищать, менять, сбрасывать и управлять паролями.

Основные технологии архитектуры

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

Кластер Redis

Redis — это хранилище структур данных в памяти с открытым исходным кодом, которое создает кэши и базы данных NoSQL типа «ключ-значение». Redis Cluster — это специальная версия Redis, которая помогает улучшить масштабируемость и доступность вашей базы данных Redis. В частности, это распределенная реализация Redis, которая автоматически разбивает (то есть разделяет) данные на несколько узлов Redis.

Redis Cluster помогает улучшить масштабируемость, доступность и отказоустойчивость баз данных Redis по сравнению с базовой версией Redis. Особенности Redis Cluster включают в себя:

  • Масштабируемость: Redis Cluster может масштабироваться до максимального предела в 1000 узлов.
  • Доступность. Есть два условия для продолжения работы кластера Redis: большинство первичных узлов должны быть доступны, а любой недостижимый первичный узел должен иметь резервный вторичный узел. Это щедрая политика, которая помогает повысить доступность вашей базы данных Redis.
  • Безопасность записи: Redis Cluster пытается вести себя безопасным для записи способом. Он попытается сохранить записи от любого клиента, подключенного к большинству первичных узлов в кластере.

HAProxy

HAProxy (High Availability Proxy) — это балансировщик нагрузки TCP/HTTP и прокси-сервер, который позволяет веб-серверу распределять входящие запросы между несколькими конечными точками. Это полезно в тех случаях, когда слишком много одновременных подключений перегружают возможности одного сервера. Вместо подключения клиента к одному серверу, который обрабатывает все запросы, клиент будет подключаться к экземпляру HAProxy, который будет использовать обратный прокси-сервер для пересылки запроса на одну из доступных конечных точек на основе алгоритма балансировки нагрузки.

Критор Фреймворк

Ktor — это асинхронная платформа для создания микросервисов, веб-приложений и многого другого. Написано на Котлине с нуля.

Высокопроизводительный дистрибутив JDK GraalVM

GraalVM — это виртуальная машина Java и JDK на основе HotSpot/OpenJDK, реализованные на Java. Он поддерживает дополнительные языки программирования и режимы выполнения, такие как заблаговременная компиляция приложений Java для быстрого запуска и уменьшения объема памяти.

Нетти-сервер

Netty — это клиент-серверная среда NIO, которая позволяет быстро и легко разрабатывать сетевые приложения, такие как серверы протоколов и клиенты. Это значительно упрощает и оптимизирует сетевое программирование, такое как серверы сокетов TCP и UDP.

SMS-API Twilio

Программируемый SMS API Twilio поможет вам добавить в ваши приложения надежные возможности обмена сообщениями.

Используя этот REST API, вы можете отправлять и получать SMS-сообщения, отслеживать доставку отправленных сообщений, планировать отправку SMS-сообщений позже, а также получать и изменять историю сообщений.

Облачные сообщения Firebase

Firebase Cloud Messaging (FCM) — это кроссплатформенное решение для обмена сообщениями, которое позволяет надежно и бесплатно отправлять сообщения.

SendGrid

SendGrid — это облачный SMTP-провайдер, который позволяет отправлять электронные письма без поддержки почтовых серверов. SendGrid управляет всеми техническими деталями, от масштабирования инфраструктуры до охвата интернет-провайдеров и мониторинга репутации до услуг белого списка и аналитики в реальном времени.

Обзор архитектуры

В этом разделе я хотел бы более подробно остановиться на работе архитектуры, хотя, во-первых, было бы интересно узнать основные цели, которые я имел в виду, когда предлагал эту конструкцию:

  • Более быстрое время запуска: создание предварительно скомпилированных микросервисов, которые запускаются за миллисекунды и обеспечивают максимальную производительность без прогрева.
  • Низкое использование ресурсов: создание заранее скомпилированных микросервисов, которые используют лишь часть ресурсов, требуемых JVM, означает, что их запуск и повышение эффективности использования обходятся дешевле.
  • Небольшой образ контейнера: Попытка сжать собственные исполняемые файлы в облегченных образах контейнеров для более безопасного, быстрого и эффективного развертывания.
  • Минимизация уязвимости: попытка уменьшить площадь поверхности атаки с помощью собственного образа путем удаления всех неиспользуемых классов, методов и полей из вашего приложения и библиотек, а также усложнения обратного проектирования путем преобразования байт-кода Java в собственный машинный код.
  • Использование универсальной и эффективной системы хранения на основе оперативной памяти с возможностью сохранения данных во вторичном хранилище для восстановления после сбоев обеспечивает высокую доступность и масштабируемость.
  • Внедрите централизованный репозиторий конфигурации. Микросервисы загрузят последнюю версию сохраненной конфигурации.
  • Включая балансировщик нагрузки на основе алгоритма Round Robin, чтобы минимизировать время отклика, повысить производительность службы и избежать насыщения.

С учетом всего вышеизложенного архитектурный проект выглядит следующим образом:

Эту архитектуру можно использовать как автономную службу проверки подлинности или как часть более сложного решения MFA. Клиенты будут запрашивать одноразовый код или пароль для подтверждения своей личности. Они укажут службу доставки, через которую они хотят получить токен (в настоящее время доступны электронная почта, SMS и push-уведомления).

Система сгенерирует OTP-токен, применяя правила, относящиеся к указанной службе доставки. Сгенерированный токен сохранится в кластере Redis с TTL, связанным с типом службы, и вернет клиенту уникальный идентификатор операции, который можно использовать для последующих операций проверки — отмены или повторной отправки.

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

Как вы можете видеть на картинке выше, в этой архитектуре мы можем выделить несколько дифференцированных частей:

Высокопроизводительный балансировщик нагрузки TCP/HTTP

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

Ниже мы покажем вам, как HAProxy был реализован в этом архитектурном подходе с использованием Round Robin. Однако сначала давайте рассмотрим алгоритм Round Robin, который предлагает HAProxy.

Алгоритм Round Robin используется чаще всего. Он использует каждую службу за балансировщиком нагрузки по очереди в соответствии с их весом. Это также, вероятно, самый плавный и честный алгоритм, поскольку время обработки сервером остается равномерно распределенным. В качестве динамического алгоритма Round Robin позволяет корректировать веса серверов на ходу.

Конфигурация балансировщика нагрузки выполняется через файл haproxy.cfg, в котором мы можем выделить два различных раздела: бэкэнд и внешний интерфейс.

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

  • Какой алгоритм балансировки нагрузки использовать
  • Список серверов и портов

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

backend otp_services
  mode http
  option forwardfor
  balance roundrobin
  server otp_service_1 otp_service_1:8080 check
  server otp_service_2 otp_service_2:8080 check
  server otp_service_3 otp_service_3:8080 check
  server otp_service_4 otp_service_4:8080 check
  server otp_service_5 otp_service_5:8080 check
  server otp_service_6 otp_service_6:8080 check

balance roundrobin строка указывает алгоритм балансировки нагрузки

mode http указывает, что будет использоваться прокси уровня 7.

Параметр check в конце директив server указывает, что проверки работоспособности должны выполняться на этих внутренних серверах.

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

  • Набор IP-адресов и портов (например, 10.1.1.7:80, *:443 и т. д.)
  • ACL
  • use_backend правила, которые определяют, какие серверные части использовать в зависимости от того, какие условия ACL совпадают, и/или default_backend правило, которое обрабатывает все остальные случаи.
frontend balancer
  bind 0.0.0.0:9090
  mode http
  default_backend otp_services

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

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

Ниже вы можете просмотреть полный файл конфигурации:

global
  stats socket /var/run/api.sock user haproxy group haproxy mode 660 level admin expose-fd listeners
  log stdout format raw local0 info
defaults
  mode http
  timeout client 10s
  timeout connect 5s
  timeout server 10s
  timeout http-request 10s
  log global
frontend stats
  bind *:8404
  stats enable
  stats uri /
  stats refresh 10s
frontend balancer
  bind 0.0.0.0:9090
  mode http
  default_backend otp_services
backend otp_services
  mode http
  option forwardfor
  balance roundrobin
  server otp_service_1 otp_service_1:8080 check
  server otp_service_2 otp_service_2:8080 check
  server otp_service_3 otp_service_3:8080 check
  server otp_service_4 otp_service_4:8080 check
  server otp_service_5 otp_service_5:8080 check
  server otp_service_6 otp_service_6:8080 check

Мы можем увидеть графическое представление состояния сервера и времени безотказной работы, посетив /haproxy?status, как показано на изображении ниже:

Облегченные облачные микросервисы на основе собственных образов GraalVM

Новые современные приложения создаются для облака в виде распределенных систем, разработанных с использованием микросервисов. Управляемый событиями, асинхронный и реактивный дизайн должен быстро и эффективно масштабироваться. Компилятор HotSpot JVM и JIT не является идеальной средой для поддержки таких вариантов использования и разработки облачных решений из-за того, как работает компилятор. Требует много памяти и процессора.

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

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

Микросервисы, реализованные Java HotSpot VM, гарантируют, что только определенные участки кода, которые часто выполняются, будут скомпилированы в машинный код, поэтому производительность приложения зависит в первую очередь от того, насколько быстро выполняются эти участки кода. Эти критические разделы известны как горячие точки приложения; Отсюда и название Java HotSpot VM.

В то время как JVM интерпретирует байт-код, JIT анализирует выполнение и динамически компилирует часто выполняемый байт-код в машинный код. Это избавляет JVM от необходимости снова и снова интерпретировать один и тот же байт-код.

Реализация Graal VM обеспечивает лучшую реализацию JIT-компилятора с дополнительной оптимизацией. Компилятор Graal также предоставляет возможность компиляции Graal AOT с опережением времени (AOT) для создания собственных образов, которые могут работать автономно со встроенными виртуальными машинами.

Используя технологию GraalVM Native Image, мы можем заранее скомпилировать сервисы в собственный код таким образом, чтобы результирующий двоичный файл не зависел от JVM для выполнения. Этот исполняемый файл можно поместить как отдельное приложение в контейнер и запустить очень, очень быстро.

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

# ---- Building phase ----
FROM ghcr.io/graalvm/native-image:22.2.0 AS builder
RUN mkdir -p /tmp/export/lib64 \
    && cp /usr/lib64/libstdc++.so.6.0.25 /tmp/export/lib64/libstdc++.so.6 \
    && cp /usr/lib64/libz.so.1 /tmp/export/lib64/libz.so.1
COPY --chown=gradle:gradle .. /home/gradle/src
WORKDIR /home/gradle/src
RUN ./gradlew clean nativeCompile --no-daemon --debug
# ---- Release ----
FROM gcr.io/distroless/base AS release
COPY --from=builder /tmp/export/lib64 /lib64
COPY --from=builder /home/gradle/src/build/native/nativeCompile/otp_graalvm_service app
ENV LD_LIBRARY_PATH /lib64
EXPOSE 8080
ENTRYPOINT ["/app"]

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

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

java -agentlib:native-image-agent=config-merge-dir=./config -jar otp_service.jar

Во время выполнения агент взаимодействует с виртуальной машиной Java для перехвата всех вызовов, которые ищут классы, методы, поля, ресурсы или запрашивают доступ к прокси. Затем агент создает файлы jni-config.json, reflect-config.json, proxy-config.json и resource-config.json в указанном выходном каталоге. Сгенерированные файлы представляют собой автономные файлы конфигурации в формате JSON, которые содержат все перехваченные динамические обращения.

Может потребоваться запустить целевое приложение более одного раза с разными входными данными, чтобы активировать отдельные пути выполнения для лучшего охвата динамических доступов. Агент поддерживает это с помощью параметра config-merge-dir, который добавляет перехваченные обращения к существующему набору файлов конфигурации.

Поскольку необходимо запускать целевое приложение более одного раза с разными входными данными, чтобы активировать отдельные пути выполнения для лучшего охвата динамических доступов. Агент поддерживает это с помощью параметра config-merge-dir, который добавляет перехваченные обращения к существующему набору файлов конфигурации, как я использовал в приведенной выше команде.

Эти файлы необходимо предоставить во время сборки собственного образа Gradle с помощью параметров ReflectionConfigurationFile и ResourceConfigurationFiles.

В результате у нас есть легкие сервисы, которые потребляют всего около 20 МБ ОЗУ по сравнению со 130 МБ при использовании образа на основе HotSpot, как вы можете видеть на следующем изображении статистики контейнера:

Асинхронные сервисы на базе Ktor Framework

Ktor — это асинхронная веб-инфраструктура, написанная и разработанная для Kotlin, использующая сопрограммы и позволяющая писать асинхронный код без необходимости самостоятельно управлять потоками.

Вот еще немного справочной информации о Ktor. Его поддерживают Jetbrains, которые также являются создателями самого Kotlin. Кто лучше создаст веб-фреймворк на Kotlin, чем люди, которые работают над языком.

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

Внедрение зависимостей от Koin

Ktor считается микрофреймворком, поэтому в нем отсутствуют некоторые функции и утилиты, которые мы могли бы найти прямо из коробки в более сложных фреймворках, таких как Spring Framework. Один из них не имеет контейнера IoC. Несмотря на это, мы можем очень просто интегрировать внешние библиотеки, такие как Kodein или Koin. В этом случае мы выбрали koin, умную библиотеку внедрения зависимостей Kotlin.

Определить зависимости модулей в Koin очень просто, вот выдержка из декларации клиента Jedis, которая позволяет нам взаимодействовать с кластером Redis:

val redisModule = module {
    single {
        JedisCluster(hashSetOf(*get<RedisClusterConfig>().nodes.map {
            HostAndPort(it.host, it.port) }.toTypedArray()) )
    }
}

Архитектура плагина

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

Такие плагины, как ktor-server-request-validation, использовались для проверки данных, полученных в запросах, а плагин ktor-server-status-pages-jvm — для адекватного управления внутренними ошибками, возникающими при обработке запросов.

Легко развертывается

Серверное приложение Ktor может быть легко доставлено в виде автономного пакета, нам нужно только сначала создать сервер. Конфигурация сервера может включать в себя различные параметры: механизм сервера (например, Netty, Jetty и т. д.), различные параметры, относящиеся к конкретному механизму, значения хоста и порта и т. д.

Функция embeddedServer — это простой способ настроить параметры сервера в коде и быстро запустить приложение.

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        configureKoin()
        configureAdministration()
        configureSerialization()
        configureValidation()
        configureMonitoring()
        configureAuthentication()
        configureRouting()
    }.start(wait = true)
}

Легко расширить и охватить широкий спектр потребностей

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

Очень просто создать дополнительный AuthenticationProvider для проверки клиентов, которые используют услуги платформы.

Кластер Redis для масштабируемости и высокой доступности

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

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

Используя эту функцию, я смог достичь следующих целей:

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

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

port 6379
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 192.168.1.39
cluster-announce-port 6379
cluster-announce-bus-port 16379
appendonly yes
loadmodule /usr/lib/redis/modules/rejson.so

Для включения режима кластера необходимо установить директивуcluster-enabled в yes. Каждый экземпляр также содержит путь к файлу, в котором хранится конфигурация для этого узла, который по умолчанию равен nodes.conf.

Все узлы Redis будут основаны на образе redislabs/rejson, предоставленном модулем rejson, для хранения контента и работы с ним в собственном формате JSON. в файле конфигурации необходимо явно загрузить этот модуль с помощью директивы loadmodule.

Кроме того, необходимо будет настроить TCP-порт Redis и порт кластерной шины, чтобы разрешить связь внутри и вне конфигурации кластера.

# Redis Node 1
  redis-node-1:
      image: 'redislabs/rejson:latest'
      container_name: redis-node-1
      command: redis-server /usr/local/etc/redis/redis.conf
      volumes:
        - ./data:/var/lib/redis
        - ./conf/node_1/redis.conf:/usr/local/etc/redis/redis.conf
      ports:
        - 6379:6379
        - 16379:16379
      networks:
        redis_cluster_network:
          ipv4_address: 192.168.0.30

Каждому узлу кластера Redis требуется два открытых TCP-подключения: TCP-порт Redis, используемый для обслуживания клиентов, например, 6379, и второй порт, известный как порт кластерной шины. По умолчанию порт шины кластера устанавливается путем добавления 10000 к порту данных (например, 16379). Однако вы можете переопределить это в конфигурации cluster-port.

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

Чтобы упростить развертывание Redis Cluster, я реализовал задачу Ruby Rake, чтобы унифицировать процесс запуска и запуска развертывания Docker Compose и создания кластера с помощью интерфейса командной строки Redis.

desc "Start and configure Cluster Containers"
  task :start => [ :check_docker_task, :login, :check_deployment_file ] do 
   puts "Start Cluster Containers"
   puts `docker-compose -f ./redis_cluster/docker-compose.yml up -d`
   puts `docker run -it --rm --network=redis_cluster_redis_cluster_network redislabs/rejson:latest redis-cli --cluster create 192.168.0.30:6379 192.168.0.35:6380 192.168.0.40:6381 192.168.0.45:6382 192.168.0.50:6383 192.168.0.55:6384 192.168.0.60:6385 192.168.0.65:6386 --cluster-replicas 1 --cluster-yes`
  end

Здесь используется команда create, поскольку мы хотим создать новый кластер. Параметр --cluster-replicas 1 означает, что нам нужна реплика для каждого созданного первичного.

Другие аргументы — это список адресов экземпляров, которые я хочу использовать для создания нового кластера.

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

[OK] All 16384 slots covered

Это означает, что существует как минимум один основной экземпляр, обслуживающий каждый из 16 384 доступных слотов.

Благодаря представлению RedisInsight в режиме реального времени мы можем проверять информацию, связанную с каждым ключом, и даже манипулировать ею. На следующем изображении показано представление JSON сгенерированной модели OTP, связанной с кодом операции 2dc15cf8–1761–48c9-b15d-cd348460a218.

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

Службы доставки одноразовых паролей

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

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

Выбор отправителя производится в момент генерации токена OTP. Вызов создания конечной точки поддерживает поля type и destination.

Вы также можете передать кучу свойств, если хотите настроить текстовое сообщение через свойство «свойства».

curl --location --request POST 'http://localhost:9090/otp/v1/generate' \
--header 'ClientId: /0GiNd8HKN3PKjOedxi9g3+7oz14gLLLg4fIRGHHSTc=' \
--header 'Content-Type: application/json' \
--data-raw '{
"type": "SMS",
"destination": "+34677112233", 
"properties": {}
}'

В приведенном выше примере мы пытаемся сгенерировать токен OTP, который будет доставлен получателю через SMS. Необходимо учитывать использование заголовка ClientId, который позволяет нам идентифицировать себя как авторизованного клиента для использования услуг.

Предыдущий запрос активирует выполнение OTP SMS Sender, как показано ниже:

Этот отправитель будет использовать библиотеку Twilio для отправки SMS с настроенным текстовым сообщением вместе с кодом OTP, который мы ранее сгенерировали. Если во время отправки произойдет ошибка, мы сообщим об исключении OTPSenderFailedException, которое отклонит код OTP и прервет процесс.

Подход был бы аналогичным, если бы мы хотели использовать службу электронной почты для доставки кода OTP, просто нам нужно было бы настроить тип отправителя на «ПОЧТА» и указать адрес электронной почты в пункте назначения.

curl --location --request POST 'http://localhost:9090/otp/v1/generate' \
--header 'ClientId: /0GiNd8HKN3PKjOedxi9g3+7oz14gLLLg4fIRGHHSTc=' \
--header 'Content-Type: application/json' \
--data-raw '{
"type": "MAIL",
"destination": "[email protected]",
"properties": {}
}'

Таким образом, мы активируем выполнение OTP Mail Sender, который будет использовать библиотеку SendGrid для создания и отправки электронной почты в указанное место назначения. Во время создания электронного письма будет указан желаемый шаблон для представления содержимого сообщения. Эти шаблоны можно создать с помощью инструментов WYSWYG, предоставляемых SendGrid.

Используемая технология

  • Кластерная архитектура Redis (модуль rejson включен)
  • Балансировщик нагрузки HAProxy
  • Критор Фреймворк
  • Нетти-сервер
  • Высокопроизводительный дистрибутив JDK GraalVM
  • Вспомогательная библиотека Java для Twilio
  • Вспомогательная библиотека Java Sendgrid
  • Облачные сообщения Firebase
  • Jedis (Java-клиент Redis, разработанный для повышения производительности и простоты использования)
  • Hoplite (библиотека конфигурации Kotlin без шаблонов для загрузки файлов конфигурации в виде классов данных)

Это оно. Мне очень понравилось разрабатывать и документировать этот небольшой проект. Спасибо, что прочитали это. Я надеюсь, что это первый из многих.

Если вас интересует полный код, вот ссылка на общедоступный репозиторий: