Опирается на правила:
AUTH-7…AUTH-9из Auth Patterns Style Guide → раздел 3. RBAC: маппинг ролей.
Важно знать
- Роли в JWT —
realm_access.roles(Keycloak) илиscope(стандартный OAuth2).- Префикс
ROLE_добавляетJwtAuthenticationConverter.hasRole('ADMIN')matchesROLE_ADMIN.- Каталог ролей UCP:
customer,seller,admin,system. Любая новая роль — пересмотр Bounded Context.@PreAuthorizeобязательна на каждом REST-endpoint. Endpoint без проверки роли — критическое нарушение.- RBAC отвечает endpoint-level, не resource-level. «Этот ли его order» — это ABAC.
RBAC (Role-Based Access Control) — первый слой авторизации после JWT validation. Отвечает на простой вопрос: «может ли пользователь этой роли вообще обращаться к этому endpoint». UCP формулирует минимум ролей — слишком много ролей означает либо плохую модель, либо ABAC, спрятанный под видом ролей.
JwtAuthenticationConverter
AUTH-7: маппинг claims → authorities.
@Configuration
public class JwtConverterConfig {
@Bean
JwtAuthenticationConverter jwtAuthConverter() {
var authorities = new JwtGrantedAuthoritiesConverter();
authorities.setAuthorityPrefix("ROLE_");
authorities.setAuthoritiesClaimName("realm_access.roles");
var converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authorities);
converter.setPrincipalClaimName("sub");
return converter;
}
}
Что делает:
- Извлекает
realm_access.rolesмассив из JWT. - Каждую строку префиксует
ROLE_. - Кладёт в
Authentication.getAuthorities(). - В
SecurityContextHolderдоступ к principal черезsubclaim.
Для Keycloak JWT:
{
"sub": "user-42",
"realm_access": {
"roles": ["customer", "loyalty-member"]
}
}
→ Spring создаёт Authentication с authorities [ROLE_customer, ROLE_loyalty-member]. Principal name = "user-42".
Для стандартного OAuth2 — scope claim, конвертер чуть другой:
authorities.setAuthoritiesClaimName("scope");
authorities.setAuthorityPrefix("SCOPE_");
И в коде @PreAuthorize("hasAuthority('SCOPE_order.read')"). В UCP-сервисах преимущественно Keycloak — поэтому используем ROLE_ префикс.
Каталог ролей UCP
AUTH-8: четыре стандартных роли.
| Роль | Кто | Что делает |
|---|---|---|
customer | Конечный пользователь | Создаёт и читает свои заказы, оплачивает |
seller | Продавец на маркетплейсе | Управляет своими товарами, видит заказы своих товаров |
admin | Внутренний пользователь | Полный доступ, всегда + audit log |
system | Service-to-service | Cross-service вызовы (Client Credentials или mTLS) |
Любая новая роль — повод пересмотреть Bounded Context. Если появляется customer-vip, premium-seller, partner-admin — обычно это не новая роль, а атрибут на существующей роли. Это уже ABAC.
Сценарии «зачем нам новая роль»:
- «У нас есть premium-customers, им доступны другие endpoints» — на самом деле это feature flag или подписка, проверяется в Handler-е по атрибуту
customer.isPremium. - «B2B-клиенты не могут видеть retail» — это разные Bounded Context, разные сервисы.
- «Junior-admin может только просматривать» — это уже сложно, обычно решается feature/permission system, не ролью.
Минимизация ролей — дисциплина.
@PreAuthorize обязательна
AUTH-9: каждый endpoint имеет проверку.
@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
public class OrderController {
private final UseCaseDispatcher dispatcher;
@PostMapping
@PreAuthorize("hasRole('customer')")
public OrderResponse create(@RequestBody @Valid CreateOrderRequest request) {
return dispatcher.dispatch(new CreateOrderCommand(request));
}
@GetMapping("/{id}")
@PreAuthorize("hasAnyRole('customer', 'admin')")
public OrderResponse get(@PathVariable Long id) {
return dispatcher.dispatch(new GetOrderByIdQuery(id));
}
@PostMapping("/{id}/cancel")
@PreAuthorize("hasAnyRole('customer', 'admin')")
public OrderResponse cancel(@PathVariable Long id) {
return dispatcher.dispatch(new CancelOrderCommand(id));
}
}
@RestController
@RequestMapping("/admin/orders")
@RequiredArgsConstructor
public class AdminOrderController {
private final UseCaseDispatcher dispatcher;
@PostMapping("/{id}/refund")
@PreAuthorize("hasRole('admin')")
public OrderResponse refund(@PathVariable Long id) {
return dispatcher.dispatch(new RefundOrderCommand(id));
}
}
Endpoint без @PreAuthorize — даже если он внутри /admin/* namespace — открыт всем аутентифицированным пользователям. Любой holder JWT может вызвать.
ArchUnit-правило для проверки:
@ArchTest
public static final ArchRule allControllersHavePreAuthorize =
methods()
.that().areDeclaredInClassesThat().areAnnotatedWith(RestController.class)
.and().areAnnotatedWith(GetMapping.class).or().areAnnotatedWith(PostMapping.class)
.or().areAnnotatedWith(PutMapping.class).or().areAnnotatedWith(DeleteMapping.class)
.should().beAnnotatedWith(PreAuthorize.class);
Это ловит «забыл аннотацию» на ревью автоматически.
RBAC ≠ ABAC
RBAC отвечает endpoint-level: «может ли роль customer обращаться к GET /orders/{id}».
RBAC не отвечает: «может ли customer-42 читать order-12345». Это уже ABAC — нужно загрузить order, посмотреть customerId, сравнить с jwt.sub. Подробнее — ABAC.
@PostMapping("/{id}/cancel")
@PreAuthorize("hasAnyRole('customer', 'admin')") // RBAC — может ли вообще
public OrderResponse cancel(@PathVariable Long id) {
return dispatcher.dispatch(new CancelOrderCommand(id));
}
@UseCase
public class CancelOrderHandler implements UseCaseHandler<CancelOrderCommand, Order> {
@Override
@Transactional
public Order handle(CancelOrderCommand command) {
var order = orderRepository.findById(command.orderId()).orElseThrow();
if (!accessChecker.canCancelOrder(order, currentUser())) {
throw new ForbiddenException();
}
order.cancel();
return orderRepository.save(order);
}
}
Два слоя — без одного из них дыра.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Endpoint без @PreAuthorize | AUTH-9 | каждый endpoint имеет проверку |
| 10+ ролей в каталоге | AUTH-8 | customer/seller/admin/system + ABAC |
Новая роль для feature flag (customer-premium) | AUTH-8 | атрибут на customer, проверка в handler |
| Hardcoded role names в коде россыпью | AUTH-9 | константы или enum |
@PreAuthorize без hasRole (просто isAuthenticated()) для critical | AUTH-9 | явная роль |
RBAC на resource-level (hasRole(...) && order.belongsToUser(...)) | AUTH-3 | RBAC endpoint + ABAC handler |
JwtAuthenticationConverter без префикса ROLE_ | AUTH-7 | setAuthorityPrefix("ROLE_") |
| Authorities из произвольного claim без convention | AUTH-7 | realm_access.roles (Keycloak) |
Куда дальше
- Auth → раздел 3. RBAC — нормативные формулировки.
- JWT validation — конфиг
oauth2ResourceServer. - ABAC: владение ресурсом — следующий слой после RBAC.
- Где какая проверка — Gateway vs BFF vs Domain.
- Audit admin-команд —
adminrole всегда + audit log. - REST API → headers — Bearer, Authorization.