Опирается на правила: AUTH-1AUTH-3 из Auth Patterns Style Guide → раздел 1. Где какая проверка делается.

Важно знать

  • Gateway / API edge — аутентификация: валидация JWT, rate limiting. Прокидывает identity вниз.
  • BFF / Application Layer — грубая авторизация по роли (RBAC) через @PreAuthorize.
  • Domain Service — авторизация по ресурсу (ABAC): order.customerId == jwt.sub.
  • ABAC никогда на Gateway — Gateway не знает доменную модель.
  • Каждый слой имеет одну ответственность: Gateway не делает RBAC, Domain не делает JWT validation.
  • Размытие ответственности — главная причина дыр в авторизации.

Auth — это не один монолитный шаг, а три разных проверки на трёх разных уровнях. Каждый уровень имеет своё знание и свою задачу. Смешение приводит либо к дублированию проверок (с шансом расхождения), либо к пропускам — endpoint без правильного слоя становится backdoor-ом.

Три уровня и три ответственности

УровеньЧто проверяетЧто знает
Gateway / API edgeподпись JWT, exp, iss, aud, rate limitTLS, IdP, JWK Set
BFF / Application LayerRBAC: есть ли роль для этого endpointendpoint paths, role catalog
Domain ServiceABAC: владеет ли этот user этим ресурсомaggregate, ownership, бизнес-инварианты

Gateway — аутентификация

AUTH-1: Gateway отвечает на вопрос «кто этот клиент».

Что делает:

  • Извлекает Authorization: Bearer <jwt> из incoming request.
  • Валидирует подпись через JWK Set (cached).
  • Проверяет exp, iss, aud.
  • Применяет rate limit per-token / per-IP.
  • Прокидывает identity в downstream (через тот же Authorization header или через X-User-Id, X-User-Roles для internal-сетей).

Что не делает:

  • Не знает endpoint paths и роли.
  • Не знает бизнес-модель.
  • Не проверяет владение ресурсом.

При невалидном JWT — 401 Unauthorized, downstream сервисы не запрашиваются вообще.

client → POST /orders + Bearer xxx
         ↓
       Gateway
         ↓ (если JWT валиден)
       order-service: знает, что пришёл от user-42

В UCP-сервисах Gateway — это Spring Cloud Gateway или Istio + JWT-filter, либо просто oauth2ResourceServer в самом сервисе если outer Gateway отсутствует. Технически проверка та же.

BFF — грубая авторизация по роли

AUTH-2: BFF/Application отвечает на вопрос «может ли этот клиент вообще обратиться к этому endpoint».

@RestController
@RequestMapping("/admin/orders")
public class AdminOrderController {

    @PostMapping("/{id}/refund")
    @PreAuthorize("hasRole('ADMIN')")
    public Order refund(@PathVariable Long id) {
        return dispatcher.dispatch(new RefundOrderCommand(id));
    }
}

@RestController
@RequestMapping("/orders")
public class OrderController {

    @GetMapping("/{id}")
    @PreAuthorize("hasAnyRole('CUSTOMER', 'ADMIN')")
    public OrderResponse get(@PathVariable Long id) {
        return dispatcher.dispatch(new GetOrderByIdQuery(id));
    }
}

RBAC отвечает на endpoint-level вопросы:

  • POST /admin/* — только ADMIN.
  • GET /orders/*CUSTOMER или ADMIN.
  • POST /ordersCUSTOMER (только клиент создаёт заказы).

Если роль не подходит — 403 Forbidden от Spring Security ещё до входа в handler.

Что BFF не делает: не проверяет, чей конкретно order. Этим занимается следующий слой.

Domain Service — авторизация по ресурсу

AUTH-3: Domain отвечает на вопрос «может ли этот клиент работать с этим конкретным ресурсом».

@UseCase
@RequiredArgsConstructor
public class GetOrderByIdHandler implements UseCaseHandler<GetOrderByIdQuery, Order> {

    private final OrderRepository orderRepository;

    @Override
    @Transactional(readOnly = true)
    public Order handle(GetOrderByIdQuery query) {
        var order = orderRepository.findById(query.orderId())
            .orElseThrow(() -> new OrderNotFoundException(query.orderId()));

        var currentUserId = SecurityContextHolder.getContext().getAuthentication().getName();
        if (!order.getCustomerId().equals(Long.valueOf(currentUserId))
            && !hasAdminRole()) {
            throw new ForbiddenException("Order does not belong to current user");
        }
        return order;
    }
}

ABAC отвечает: загрузили order=12345, у него customerId=42, текущий user sub=99 — отказ. Это не RBAC: роль CUSTOMER валидна, endpoint доступен, но этот customer не имеет права читать чужой заказ.

Подробнее — ABAC.

Почему ABAC не на Gateway

AUTH-3 (запрет): Gateway не знает доменную модель.

Сценарий, где это ломается:

  1. Gateway получает GET /orders/12345, JWT валиден, user sub=99.
  2. Gateway пытается решить ABAC: «чей order 12345?» — нужно сходить в DB или в order-service.
  3. Чтобы решить, Gateway фактически становится сервисом, дублирует домен.
  4. При изменении модели (например, добавили co-owners) — Gateway надо обновлять параллельно с order-service.

Это размывает ответственность и создаёт двойной источник правды. Корректно: Gateway только аутентификация, ABAC — внутри Domain Service, где живёт Order agg.

Что запрещено

АнтипаттернПравилоЧто взамен
RBAC на GatewayAUTH-2Gateway только аутентификация
ABAC на Gateway или BFFAUTH-3ABAC в Domain Service
JWT validation внутри HandlerAUTH-1OAuth2 Resource Server на edge
Endpoint без @PreAuthorizeAUTH-2 (нарушение)@PreAuthorize("hasRole(...)") обязательно
Только RBAC, без ABAC для own-resource endpointsAUTH-3RBAC на endpoint + ABAC в handler
Дублирование JWT-проверки на каждом слоеAUTH-1один раз на Gateway/edge
Custom JWT-filter вместо OAuth2 Resource ServerAUTH-4стандартный Spring Security

Куда дальше

  • Auth → раздел 1. Где какая проверка — нормативные формулировки.
  • JWT validation — стандартный Spring Security flow.
  • RBAC: маппинг ролей — @PreAuthorize, role catalog.
  • ABAC: владение ресурсом — @access.canEditOrder.
  • Service-to-service — mTLS, Client Credentials.
  • Audit admin-команд — *_audit_log обязателен для admin.