Когда пользователь отправляет запрос, приложение должно ответить на три разных вопроса:
- Кто это вообще? — токен настоящий, не истёк, не подделан?
- Может ли он обратиться к этому endpoint? — у него есть нужная роль?
- Может ли он работать именно с этим ресурсом? — это его заказ, или чужой?
Это три разных вопроса, и каждый из них задаётся на своём уровне. Если смешать их в одном месте — получается либо дублирование с риском расхождений, либо дыры: один 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 — как сервисы аутентифицируют друг друга.