Опирается на правила:
AUTH-1…AUTH-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 limit | TLS, IdP, JWK Set |
| BFF / Application Layer | RBAC: есть ли роль для этого endpoint | endpoint paths, role catalog |
| Domain Service | ABAC: владеет ли этот 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 (через тот же
Authorizationheader или через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 /orders—CUSTOMER(только клиент создаёт заказы).
Если роль не подходит — 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 не знает доменную модель.
Сценарий, где это ломается:
- Gateway получает
GET /orders/12345, JWT валиден, usersub=99. - Gateway пытается решить ABAC: «чей order 12345?» — нужно сходить в DB или в order-service.
- Чтобы решить, Gateway фактически становится сервисом, дублирует домен.
- При изменении модели (например, добавили co-owners) — Gateway надо обновлять параллельно с order-service.
Это размывает ответственность и создаёт двойной источник правды. Корректно: Gateway только аутентификация, ABAC — внутри Domain Service, где живёт Order agg.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
| RBAC на Gateway | AUTH-2 | Gateway только аутентификация |
| ABAC на Gateway или BFF | AUTH-3 | ABAC в Domain Service |
| JWT validation внутри Handler | AUTH-1 | OAuth2 Resource Server на edge |
Endpoint без @PreAuthorize | AUTH-2 (нарушение) | @PreAuthorize("hasRole(...)") обязательно |
| Только RBAC, без ABAC для own-resource endpoints | AUTH-3 | RBAC на endpoint + ABAC в handler |
| Дублирование JWT-проверки на каждом слое | AUTH-1 | один раз на Gateway/edge |
| Custom JWT-filter вместо OAuth2 Resource Server | AUTH-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.