Опирается на правила: AUTH-7AUTH-9 из Auth Patterns Style Guide → раздел 3. RBAC: маппинг ролей.

Важно знать

  • Роли в JWTrealm_access.roles (Keycloak) или scope (стандартный OAuth2).
  • Префикс ROLE_ добавляет JwtAuthenticationConverter. hasRole('ADMIN') matches ROLE_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 через sub claim.

Для 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
systemService-to-serviceCross-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 без @PreAuthorizeAUTH-9каждый endpoint имеет проверку
10+ ролей в каталогеAUTH-8customer/seller/admin/system + ABAC
Новая роль для feature flag (customer-premium)AUTH-8атрибут на customer, проверка в handler
Hardcoded role names в коде россыпьюAUTH-9константы или enum
@PreAuthorize без hasRole (просто isAuthenticated()) для criticalAUTH-9явная роль
RBAC на resource-level (hasRole(...) && order.belongsToUser(...))AUTH-3RBAC endpoint + ABAC handler
JwtAuthenticationConverter без префикса ROLE_AUTH-7setAuthorityPrefix("ROLE_")
Authorities из произвольного claim без conventionAUTH-7realm_access.roles (Keycloak)

Куда дальше

  • Auth → раздел 3. RBAC — нормативные формулировки.
  • JWT validation — конфиг oauth2ResourceServer.
  • ABAC: владение ресурсом — следующий слой после RBAC.
  • Где какая проверка — Gateway vs BFF vs Domain.
  • Audit admin-команд — admin role всегда + audit log.
  • REST API → headers — Bearer, Authorization.