← назад к разделу

Когда пользователь отправляет запрос, приложение должно ответить на три разных вопроса:

  1. Кто это вообще? — токен настоящий, не истёк, не подделан?
  2. Может ли он обратиться к этому endpoint? — у него есть нужная роль?
  3. Может ли он работать именно с этим ресурсом? — это его заказ, или чужой?

Это три разных вопроса, и каждый из них задаётся на своём уровне. Если смешать их в одном месте — получается либо дублирование с риском расхождений, либо дыры: один endpoint проверяет всё, другой — ничего.

Три уровня — три вопроса

УровеньВопросЧто проверяет
Gateway / API edgeКто это?Подпись JWT, срок действия, издатель
BFF / Application LayerМожно ли сюда обращаться?Роль пользователя (RBAC)
Domain ServiceМожно ли работать с этим объектом?Владение ресурсом (ABAC)

Gateway — кто стучится в дверь

Первый пропускной пункт — это Gateway (или сам сервис, если Gateway нет). Его задача — ответить на вопрос «кто этот клиент?».

Что Gateway делает:

  • Извлекает токен из заголовка Authorization: Bearer <jwt>.
  • Проверяет подпись токена через JWK Set (публичные ключи IdP).
  • Проверяет срок действия (exp), издателя (iss) и аудиторию (aud).
  • Ограничивает количество запросов (rate limiting).
  • Передаёт identity дальше — через тот же заголовок или через X-User-Id, X-User-Roles.

Если токен невалиден — запрос получает 401 Unauthorized, и никакие внутренние сервисы не вызываются.

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

Что Gateway не делает: он не знает, какие endpoint-ы существуют и какие роли нужны. И тем более не знает бизнес-модель — кому принадлежит заказ №12345.

В Spring Boot Gateway — это Spring Cloud Gateway или Istio с JWT-фильтром. Если внешнего Gateway нет, проверку токена делает сам сервис через oauth2ResourceServer в Spring Security — поведение то же самое.

BFF — есть ли право зайти в эту дверь

Допустим, токен валиден. Теперь второй вопрос: «может ли пользователь с его ролью обращаться к этому конкретному endpoint?».

Это называется RBAC (Role-Based Access Control) — контроль доступа на основе ролей. В Spring это делается через @PreAuthorize прямо на контроллере:

@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));
    }
}

Если роль не подходит — Spring Security вернёт 403 Forbidden ещё до того, как запрос дойдёт до бизнес-логики.

Типичное разграничение:

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

Что RBAC не проверяет: он не знает, чей именно заказ №12345. Это не его задача.

Domain Service — можно ли работать именно с этим объектом

Третий вопрос — самый тонкий: «этот пользователь имеет право читать или менять именно этот ресурс?».

Роль CUSTOMER есть у всех покупателей. Но покупатель не должен видеть чужие заказы. Это нельзя проверить по роли — нужно загрузить объект и сравнить владельца с текущим пользователем.

Такой подход называется ABAC (Attribute-Based Access Control). Он живёт внутри обработчика бизнес-логики:

@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;
    }
}

Логика: загрузили заказ №12345, у него customerId=42, текущий пользователь sub=99 — отказ. Роль CUSTOMER есть, endpoint разрешён, но этот покупатель читает чужой заказ.

Подробнее о реализации ABAC — в статье ABAC: владение ресурсом.

Почему ABAC нельзя делать на Gateway

Иногда возникает соблазн проверять всё на Gateway — чтобы «отсечь раньше». Но с ABAC это не работает.

Проблема: чтобы Gateway ответил «может ли user-99 читать заказ 12345», ему нужно знать, кому принадлежит заказ. Для этого нужно идти в базу данных или вызывать order-service. То есть Gateway фактически становится ещё одним сервисом, который понимает бизнес-модель.

Что плохого:

  • При изменении модели (например, добавили соавторов заказа) нужно обновлять и order-service, и Gateway.
  • Появляется два источника правды о том, кому принадлежит ресурс.
  • Gateway перегружается логикой, которая ему не принадлежит.

Правило: Gateway делает только аутентификацию. ABAC — только внутри Domain Service, где живёт агрегат.

Частые ошибки

Endpoint без @PreAuthorize. Если забыть аннотацию — Spring Security пропустит запрос с любой ролью. Каждый endpoint должен явно объявлять, кто к нему имеет доступ.

Только RBAC без ABAC для ресурсо-ориентированных endpoint-ов. GET /orders/{id} проверяет роль, но не владельца — любой CUSTOMER читает любой заказ.

JWT-проверка внутри Handler. Обработчик бизнес-логики не должен разбирать токен вручную — это работа OAuth2 Resource Server на уровне Edge. Handler получает уже извлечённые данные из SecurityContextHolder.

Дублирование JWT-проверки на каждом слое. Если Gateway уже проверил токен — повторять это в каждом сервисе не нужно. Достаточно доверять propagated identity в защищённой внутренней сети.

Коротко

  • Auth — это три разных проверки, не одна: аутентификация, RBAC, ABAC.
  • Gateway проверяет JWT: подпись, срок, издатель. Невалидный токен → 401, дальше запрос не идёт.
  • BFF / Controller проверяет роль через @PreAuthorize. Нет роли → 403.
  • Domain Service проверяет владение конкретным ресурсом. Чужой объект → 403.
  • ABAC нельзя делать на Gateway: Gateway не знает доменную модель.
  • Каждый endpoint должен иметь явную RBAC-аннотацию — без неё дверь открыта всем.

Что почитать дальше

  • ABAC: владение ресурсом — как реализовать проверку владения через @access-бин или внутри Handler.
  • JWT validation — стандартный Spring Security flow для проверки токенов.
  • RBAC: маппинг ролей — как настроить @PreAuthorize и каталог ролей.
  • Service-to-service — как сервисы аутентифицируют друг друга.