🏗️ Раздел 17 · Вопрос #23

Как реализовать аутентификацию и авторизацию в микросервисах

В распределённой системе задачи безопасности разделяются на два понятия:


🟢 Junior Level

В распределённой системе задачи безопасности разделяются на два понятия:

  • Аутентификация (Authentication / AuthN): Проверка подлинности — «Кто ты такой?» (логин, пароль, биометрия, SSO).
  • Авторизация (Authorization / AuthZ): Проверка прав доступа — «Что тебе разрешено делать?» (роли ROLE_ADMIN, права на чтение конкретного документа).
                      ┌───────────────────────────────────────────────┐
                      │    Централизованный Identity Provider (IdP)   │
                      │               (Keycloak / Okta)               │
                      └───────────────────────▲───────────────────────┘
                                              │ 1. Login / OAuth2 PKCE
                                              ▼ 2. JWT Access Token
[SPA / Mobile Client] ──3. Request + Bearer JWT──► [API Gateway]
                                                         │ 4. Валидация подписи (JWKS)
                                                         ▼ 5. Обогащение: X-User-Id: 42
                                                   [Order Service]

Главный стандарт: OAuth 2.0 + OpenID Connect (OIDC) + JWT

  1. Пользователь логинится один раз в едином сервисе авторизации (Identity Provider / Keycloak).
  2. Клиент получает JWT (JSON Web Token), содержащий sub (ID пользователя), roles (роли) и цифровую подпись Signature.
  3. Все последующие запросы отправляются с заголовком: Authorization: Bearer <JWT-токен>
  4. API Gateway проверяет валидность токена на входе и передает запрос во внутреннюю сеть.

🟡 Middle Level

1. Архитектурные паттерны передачи контекста пользователя

Паттерн Как устроен Достоинства Недостатки / Риски
Edge Authentication + Headers Gateway проверяет JWT, извлекает claims и пробрасывает внутренние HTTP-заголовки X-User-Id, X-User-Roles. Внутренние сервисы просты, не содержат криптографических библиотек. 🔴 Уязвимо к подделке заголовков внутри периметра (нарушение Zero Trust).
Token Relay Gateway пробрасывает входящий клиентский JWT токен без изменений через всю цепочку вызовов. Высокая безопасность: каждый сервис видит оригинального автора запроса. Высокий CPU-overhead на повторную проверку криптографической подписи JWT в каждом сервисе.
Phantom Token Pattern Клиент получает непрозрачный Reference Token (UUID). Gateway меняет его на криптографический JWT для внутренних микросервисов. Внешний клиент не видит внутренних данных JWT; безопасность максимальна. Дополнительный lookup в кэше Redis на шлюзе.

2. Реализация Resource Server в Spring Security 6 (Spring Boot 3)

Внутренний микросервис настраивается как OAuth2 Resource Server, проверяющий JWT через публичные ключи Identity Provider (JWKS):

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          # Адрес, откуда сервис автоматически скачивает публичные ключи для проверки подписи
          jwk-set-uri: http://keycloak:8080/realms/production/protocol/openid-connect/certs
          issuer-uri: http://keycloak:8080/realms/production

Конфигурация авторизации и ролевого доступа в Java:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Включает @PreAuthorize
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/public/**").permitAll()
                .requestMatchers(HttpMethod.POST, "/api/v1/orders/*").hasAuthority("SCOPE_orders.write")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .build();
    }
}

// Защита конкретного метода бизнес-логики:
@Service
public class OrderService {

    @PreAuthorize("hasRole('ADMIN') or #userId == authentication.name")
    public OrderDto getOrderForUser(Long orderId, String userId) {
        // Доступ разрешен только админу или владельцу заказа
        return orderRepository.findOrder(orderId);
    }
}

🔴 Senior Level

1. Безопасность межсервисного взаимодействия: Zero Trust & mTLS

В концепции Zero Trust (Нулевое доверие) внутренний сетевой периметр считается изначально враждебным (скомпрометированным). Если злоумышленник внедрил уязвимость в один под, он не должен иметь возможности слать поддельные заголовки X-User-Id: admin на другие сервисы.

[Service A (Order)] ──(1. mTLS + SPIFFE ID)──► [Service B (Billing)]
        │                                             │
        └── Взаимная аутентификация сертификатами x509 ┘
            (Шифрование трафика + проверка SAN: spiffe://cluster/ns/prod/sa/order-sa)
  1. Service-to-Service AuthN: Реализуется через mTLS (Mutual TLS) на уровне Service Mesh (Istio / Envoy). Стороны проверяют x509-сертификаты друг друга, гарантируя, что вызывающий сервис — это действительно order-service, а не подделка.
  2. Service-to-Service AuthZ: Использование протокола OAuth 2.0 Client Credentials Flow: сервис запрашивает в Keycloak технический сервисный токен для выполнения системных фоновых задач.

2. Проблема немедленного отзыва JWT (Revocation Problem)

JWT является stateless-токеном: если токен выпущен со сроком действия 24 часа, отозвать его невозможно без обращения к центральной базе, пока не истечет поле exp (Expiration). Если аккаунт пользователя взломан или сотрудника уволили, злоумышленник может пользоваться украденным токеном часами.

Архитектурные решения проблемы:

  1. Short-Lived Access Tokens (Короткоживущие токены):
    • Время жизни Access Token устанавливается в $5\text{–}15$ минут.
    • Клиент обновляет токен в фоне через Refresh Token, который хранится в базе данных Identity Provider с механизмом ротации (Refresh Token Rotation). При компрометации Refresh Token блокируется в IdP, и через 5 минут доступ полностью прекращается.
  2. Distributed Blacklist / Bloom Filter в Redis: При выходе из системы (Logout) jti (JWT ID) токена помещается в Redis-кэш со сроком жизни, равным оставшемуся времени до exp. Шлюз проверяет наличие jti в Redis. Для экономии памяти используется фильтр Блума (Bloom Filter).

3. Проблема «Confused Deputy» и Token Exchange (RFC 8693)

Если Сервис $A$ вызывает Сервис $B$ от имени пользователя, а Сервис $B$ вызывает Сервис $C$:

  • Если Сервис $B$ передает дальше исходный клиентский JWT, недобросовестный разработчик Сервиса $B$ может использовать токен клиента для вызова любых других сервисов компании от чужого имени.
  • Решение: OAuth 2.0 Token Exchange (RFC 8693). Сервис $B$ обменивает токен клиента в IdP на новый производный токен с ограниченным списком получателей (aud: service-c) и урезанными правами (Downscoped Scopes).

4 Tricky Questions

1. Почему проверка заголовка X-User-Id без проверки подписи во внутреннем контуре является критической уязвимостью?

Ответ: Если сервисы внутри периметра слепо доверяют HTTP-заголовкам X-User-Id и X-User-Roles, полагая, что их выставил API Gateway: Любой внутренний сервис (например, под скомпрометированной сторонней библиотекой через уязвимость Log4Shell или SSRF) может отправить HTTP-запрос напрямую в Payment Service с заголовком X-User-Roles: ROLE_SUPER_ADMIN и X-User-Id: 1. Защита:

  1. Gateway подписывает внутренний запрос облегченным внутренним асимметричным токеном.
  2. Сетевые политики Kubernetes (NetworkPolicy) и Service Mesh (Istio AuthorizationPolicy) блокируют любой входящий трафик к сервисам в обход шлюза.

2. Как микросервисы проверяют подпись JWT без сетевого похода в Keycloak на каждый запрос?

Ответ: Используется асимметричное шифрование (RS256 / ES256) и механизм JWKS (JSON Web Key Set):

  1. Keycloak подписывает токен своим приватным ключом, который никому не отдает.
  2. Микросервисы при старте один раз загружают набор публичных ключей по URL /.well-known/jwks.json и кэшируют их в оперативной памяти.
  3. Проверка подписи выполняется математически внутри процессора (in-memory) за доли миллисекунды без сетевых обращений к IdP.
  4. В заголовке JWT передается параметр kid (Key ID). Если IdP сменил ключ, сервис видит неизвестный kid, делает разовый сетевой запрос за новым JWKS и обновляет локальный кэш.

3. В чем разница между RBAC, ABAC и ReBAC при авторизации в микросервисах?

Ответ:

  • RBAC (Role-Based): Доступ определяется статическими ролями (ROLE_MANAGER, ROLE_ADMIN). Легко внедрить, но негибко: сложно выразить правило «Менеджер может редактировать заказ только своего филиала».
  • ABAC (Attribute-Based): Доступ рассчитывается динамически по атрибутам субъекта, ресурса и окружения (например, if user.dept == order.dept and time >= 09:00). Реализуется через движки политик (Open Policy Agent / OPA).
  • ReBAC (Relationship-Based): Доступ определяется графом отношений между объектами (Google Zanzibar / Ory Keto): «Пользователь может редактировать документ, если он является участником группы, которая владеет папкой, содержащей этот документ».

4. Где хранить Refresh Token в веб-приложениях (SPA), чтобы избежать кражи через XSS?

Ответ: Хранение токенов в localStorage или sessionStorage браузера крайне уязвимо: любой XSS-скрипт имеет прямой доступ к JS-хранилищу и может украсть токен. Безопасное решение: HttpOnly, Secure, SameSite=Strict Cookie. Браузер автоматически отправляет такую cookie при запросе к IdP/Gateway, но JavaScript-код (включая вредоносные XSS-скрипты) физически не имеет доступа к ее чтению через document.cookie. Для защиты от CSRF используются механизмы Double Submit Cookie или заголовок SameSite=Strict.


🎯 Шпаргалка для интервью

Главное:

  • Аутентификация — кто ты; Авторизация — что разрешено делать.
  • Стандарт индустрии: OAuth 2.0 + OpenID Connect (OIDC) с централизованным Identity Provider (Keycloak, Auth0).
  • Формат передачи данных пользователя — JWT (JSON Web Token) с асимметричной подписью (RS256).
  • Микросервисы проверяют подпись локально in-memory через публичные ключи JWKS (без похода в базу на каждый вызов).
  • Short-Lived Access Tokens (5–15 мин) + Refresh Token решают проблему невозможности отзыва JWT.
  • Принцип Zero Trust: защита межсервисных вызовов через mTLS (Service Mesh) и проверка авторизации на каждом шаге.
  • Ролевой контроль: от простого RBAC в аннотациях @PreAuthorize до ABAC / Open Policy Agent (OPA) для сложных политик.

Красные флаги (Чего говорить НЕЛЬЗЯ):

  • ❌ «JWT токены зашифрованы, в них можно безопасно хранить пароли и номера кредиток» (JWT подписан, но НЕ зашифрован; Payload закодирован в Base64Url и легко читается любым человеком).
  • ❌ «Каждый сервис должен ходить в Keycloak по HTTP на каждый запрос для проверки токена» (Это превратит IdP в гигантское узкое место; проверка подписи выполняется локально по публичному ключу).
  • ❌ «Во внутреннем контуре безопасность не нужна, внутри периметра все свои» (Грубейшее нарушение принципа Zero Trust; скомпрометированный сервис получает неограниченный доступ ко всей системе).

Связанные темы: