QuarkusSeniorCoding

Как реализовать безопасность в Quarkus с помощью @RolesAllowed и JWT?

Quarkus защищает эндпоинты через @RolesAllowed с JWT-токенами, валидируемыми либо локально (quarkus-smallrye-jwt + PEM-ключ), либо через OIDC-провайдер (quarkus-oidc). Роли берутся из клейма groups, JsonWebToken инжектируется через CDI для доступа к claims.

Механизм безопасности в Quarkus

Quarkus реализует безопасность через расширения quarkus-smallrye-jwt (локальная валидация JWT) и quarkus-oidc (OIDC/Keycloak). @RolesAllowed — стандартная Jakarta EE-аннотация, работающая с обоими вариантами.

Зависимости

<!-- Вариант 1: локальная валидация JWT -->
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>

<!-- Вариант 2: OIDC (Keycloak, Auth0 и др.) -->
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-oidc</artifactId>
</dependency>

Конфигурация SmallRye JWT

# application.properties
mp.jwt.verify.publickey.location=META-INF/resources/publicKey.pem
mp.jwt.verify.issuer=https://auth.example.com
quarkus.smallrye-jwt.blocking-authentication=true

Защита эндпоинтов через @RolesAllowed

import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.DenyAll;
import org.eclipse.microprofile.jwt.JsonWebToken;

@Path("/api/orders")
@RequestScoped
public class OrderResource {

    @Inject
    JsonWebToken jwt;  // инжекция распарсенного токена

    @GET
    @PermitAll
    public List<Order> listPublic() {
        return orderService.findPublic();
    }

    @GET
    @Path("/my")
    @RolesAllowed({"user", "admin"})
    public List<Order> myOrders() {
        String userId = jwt.getSubject();
        return orderService.findByUser(userId);
    }

    @DELETE
    @Path("/{id}")
    @RolesAllowed("admin")
    public Response delete(@PathParam("id") Long id) {
        orderService.delete(id);
        return Response.noContent().build();
    }

    @GET
    @Path("/internal")
    @DenyAll  // всегда 403, даже с токеном
    public Response internal() {
        return Response.ok().build();
    }
}

Создание и подпись JWT (для тестов / сервис-аккаунтов)

// В тестах используйте io.smallrye.jwt.build.Jwt
String token = Jwt.issuer("https://auth.example.com")
    .subject("user-123")
    .groups(Set.of("user", "admin"))
    .expiresIn(Duration.ofHours(1))
    .sign(); // подписывает приватным ключом из test-resources

Извлечение Claims

@Inject
@Claim("email")
String userEmail;

@Inject
@Claim(ClaimConstants.GROUPS)
Set<String> roles;

// Или напрямую через JsonWebToken:
String customClaim = jwt.getClaim("org_id");

Политики на уровне конфигурации (без аннотаций)

quarkus.http.auth.permission.admin-only.paths=/api/admin/*
quarkus.http.auth.permission.admin-only.policy=role-policy-admin
quarkus.http.auth.policy.role-policy-admin.roles-allowed=admin

quarkus.http.auth.permission.authenticated.paths=/api/*
quarkus.http.auth.permission.authenticated.policy=authenticated

Тестирование с @TestSecurity

@QuarkusTest
public class OrderResourceTest {

    @Test
    @TestSecurity(user = "alice", roles = {"admin"})
    public void adminCanDelete() {
        given()
            .when().delete("/api/orders/1")
            .then().statusCode(204);
    }

    @Test
    public void anonymousIsUnauthorized() {
        given()
            .when().get("/api/orders/my")
            .then().statusCode(401);
    }
}

Подводные камни

  • Роли в JWT берутся из клейма groups (MicroProfile JWT spec) — Keycloak по умолчанию кладёт их в realm_access.roles; нужна кастомная маппинг-логика или quarkus.oidc.roles.role-claim-path=realm_access/roles.
  • @RolesAllowed на уровне класса применяется ко всем методам, но метод-аннотация переопределяет её — порядок приоритетов неочевиден.
  • Без @RequestScoped или @ApplicationScoped инжекция JsonWebToken может вернуть пустой токен в не-JAX-RS контексте.
  • В native-сборке PEM-ключ должен быть в resources, а не загружаться по HTTP-URL — это ломает холодный старт при недоступности JWKS-эндпоинта.
  • Истечение токена (exp) проверяется автоматически, но clock skew между сервисами вызывает ложные 401 — настройте mp.jwt.token.age или синхронизацию NTP.
  • Алгоритм подписи по умолчанию RS256; если выдаётся HS256-токен, SmallRye JWT отклонит его — явно укажите quarkus.smallrye-jwt.token.signature-algorithm=HS256.
  • @PermitAll на методе не отменяет политику authenticated, заданную через application.properties — конфигурационные политики имеют приоритет.
  • Не смешивать quarkus-smallrye-jwt и quarkus-oidc в одном приложении без явного указания quarkus.http.auth.proactive=false — возможны конфликты механизмов аутентификации.

Common mistakes

  • Путать термин «rolesallowed jwt» с соседним механизмом Quarkus.
  • Не называть границу lifecycle, transaction, thread или request для «rolesallowed jwt».
  • Игнорировать production-эффекты «rolesallowed jwt»: latency, SQL shape, memory, security или observability.

What the interviewer is testing

  • Попросить объяснить механизм «rolesallowed jwt» на минимальном примере.
  • Проверить, видит ли кандидат failure mode и диагностику для «rolesallowed jwt».
  • Уточнить, какие настройки или API меняют «rolesallowed jwt» в реальном сервисе.

Sources

Related topics