API 하나를 만들어서 배포했는데, 인증 없이 누구나 호출할 수 있다면? 보안은 "나중에 붙이자"가 아니라, 처음부터 구조를 잡아야 하는 영역입니다. Quarkus에서 보안을 어떻게 구성하는지 처음부터 정리해봅니다.

Quarkus Security 아키텍처

Quarkus의 보안은 세 가지 핵심 컴포넌트로 구성됩니다.

한 줄 정의: 요청이 들어오면 HttpAuthenticationMechanism 이 인증 정보를 추출하고, IdentityProvider 가 신원을 확인하며, 결과로 SecurityIdentity 가 생성됩니다.

PLAINTEXT
HTTP 요청


┌──────────────────────────┐
│ HttpAuthenticationMechanism │  ← 토큰/쿠키에서 인증 정보 추출
└──────────┬───────────────┘


┌──────────────────────────┐
│     IdentityProvider     │  ← 자격증명 검증 (DB, OIDC, JWT 등)
└──────────┬───────────────┘


┌──────────────────────────┐
│     SecurityIdentity     │  ← 인증된 사용자 정보 + 역할/권한
└──────────────────────────┘


   @RolesAllowed 등으로 인가 체크

이 구조를 Spring Security와 비교하면 이렇습니다.

QuarkusSpring Security역할
HttpAuthenticationMechanismAuthenticationFilter인증 정보 추출
IdentityProviderAuthenticationProvider자격증명 검증
SecurityIdentityAuthentication인증 결과 객체
어노테이션 기반 인가SecurityFilterChain + 어노테이션접근 제어

Spring Security의 FilterChain은 유연하지만 구조가 복잡합니다. Quarkus는 이 부분을 단순화해서, 대부분의 경우 익스텐션 추가 + properties 설정 + 어노테이션 만으로 보안을 구현할 수 있습니다.


quarkus-smallrye-jwt — JWT 토큰 검증

REST API에서 가장 흔한 인증 방식인 JWT를 먼저 살펴봅니다.

의존성 추가

XML
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>

설정

PROPERTIES
# JWT 검증에 사용할 공개키 위치
mp.jwt.verify.publickey.location=publicKey.pem

# 발급자(issuer) 검증
mp.jwt.verify.issuer=https://my-auth-server.example.com

# 토큰 만료 시간 허용 오차 (초)
smallrye.jwt.expiration.grace=60

JWT 토큰 발급

JAVA
@Path("/auth")
@ApplicationScoped
public class AuthResource {

    @POST
    @Path("/login")
    @PermitAll
    public Response login(LoginRequest request) {
        // 사용자 인증 (DB 조회 등)
        User user = userService.authenticate(
            request.username(), request.password());

        if (user == null) {
            return Response.status(401).build();
        }

        // JWT 토큰 생성
        String token = Jwt.issuer("https://my-auth-server.example.com")
            .upn(user.getEmail())                    // 사용자 식별자
            .groups(user.getRoles())                 // 역할 (Set<String>)
            .claim("userId", user.getId())           // 커스텀 클레임
            .claim("displayName", user.getName())
            .expiresIn(Duration.ofHours(2))          // 만료 시간
            .sign();                                 // 개인키로 서명

        return Response.ok(new TokenResponse(token)).build();
    }
}

JWT 클레임 접근

토큰이 검증되면 JsonWebToken이나 @Claim으로 클레임에 접근할 수 있습니다.

JAVA
@Path("/users")
@ApplicationScoped
@Authenticated  // 인증된 사용자만 접근 가능
public class UserResource {

    @Inject
    JsonWebToken jwt;  // 전체 JWT 토큰 객체

    @Inject
    @Claim(standard = Claims.upn)
    String userEmail;  // UPN 클레임 직접 주입

    @Inject
    @Claim("userId")
    Long userId;  // 커스텀 클레임 주입

    @GET
    @Path("/me")
    public UserProfile getMyProfile() {
        // JsonWebToken에서 직접 접근
        String issuer = jwt.getIssuer();
        Set<String> groups = jwt.getGroups();
        long expiresAt = jwt.getExpirationTime();

        return new UserProfile(userId, userEmail, groups);
    }

    @GET
    @Path("/me/tokens")
    public TokenInfo getTokenInfo() {
        return new TokenInfo(
            jwt.getName(),
            jwt.getGroups(),
            Instant.ofEpochSecond(jwt.getExpirationTime())
        );
    }
}

공부하다 보니, SmallRye JWT는 MicroProfile JWT 표준을 구현한 것이라 표준 클레임 이름(upn, groups 등)을 사용합니다. Spring Security에서 자유롭게 클레임을 매핑하는 것과 달리 표준화된 클레임 체계가 있다는 점이 다릅니다.


quarkus-oidc — OpenID Connect 기반 인증

실제 운영 환경에서는 JWT를 직접 발급하기보다 Keycloak, Auth0, Okta 같은 OIDC Provider를 사용하는 경우가 많습니다.

의존성 추가

XML
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc</artifactId>
</dependency>

Bearer 토큰 검증 (API 서버)

PROPERTIES
# OIDC Provider 설정 (Keycloak 예시)
quarkus.oidc.auth-server-url=https://keycloak.example.com/realms/my-realm
quarkus.oidc.client-id=my-api-service

# Bearer 토큰만 검증 (서버 측 API)
quarkus.oidc.application-type=service

# 토큰 발급자 검증
quarkus.oidc.token.issuer=https://keycloak.example.com/realms/my-realm

이렇게 설정하면 Quarkus가 자동으로 다음을 처리합니다.

  1. OIDC Provider의 .well-known/openid-configuration 엔드포인트에서 설정 정보를 가져옴
  2. 공개키(JWKS)를 자동으로 다운로드
  3. 모든 요청에서 Bearer 토큰을 추출해 검증
  4. 검증 성공 시 SecurityIdentity를 생성

웹 애플리케이션 (Authorization Code Flow)

웹 애플리케이션에서 OIDC 로그인 플로우를 구현할 때는 application-type을 변경합니다.

PROPERTIES
quarkus.oidc.auth-server-url=https://keycloak.example.com/realms/my-realm
quarkus.oidc.client-id=my-web-app
quarkus.oidc.credentials.secret=web-app-secret

# Authorization Code Flow
quarkus.oidc.application-type=web-app

# 로그인 후 리다이렉트 경로
quarkus.oidc.authentication.redirect-path=/callback
quarkus.oidc.logout.path=/logout

어노테이션 기반 인가

Quarkus는 Jakarta Security 어노테이션으로 세밀한 접근 제어를 지원합니다.

@RolesAllowed — 역할 기반 접근 제어

JAVA
@Path("/admin")
@ApplicationScoped
public class AdminResource {

    @GET
    @Path("/users")
    @RolesAllowed("admin")  // admin 역할만 접근 가능
    public List<User> getAllUsers() {
        return User.listAll();
    }

    @DELETE
    @Path("/users/{id}")
    @RolesAllowed({"admin", "super-admin"})  // 여러 역할 허용
    public void deleteUser(@PathParam("id") Long id) {
        User.deleteById(id);
    }
}

@Authenticated — 인증만 요구

JAVA
@Path("/profile")
@Authenticated  // 로그인만 되어 있으면 역할 불문
public class ProfileResource {

    @Inject
    SecurityIdentity identity;

    @GET
    public UserProfile getProfile() {
        String username = identity.getPrincipal().getName();
        Set<String> roles = identity.getRoles();
        return new UserProfile(username, roles);
    }
}

@PermissionsAllowed — 권한 기반 접근 제어

Quarkus 3.x부터 지원하는 더 세밀한 권한 제어입니다.

JAVA
@Path("/documents")
@ApplicationScoped
public class DocumentResource {

    @GET
    @PermissionsAllowed("document:read")
    public List<Document> listDocuments() {
        return Document.listAll();
    }

    @POST
    @PermissionsAllowed("document:write")
    public Document createDocument(DocumentRequest request) {
        return Document.create(request);
    }

    @DELETE
    @Path("/{id}")
    @PermissionsAllowed("document:delete")
    public void deleteDocument(@PathParam("id") Long id) {
        Document.deleteById(id);
    }
}

@PermitAll과 @DenyAll

JAVA
@Path("/public")
@ApplicationScoped
public class PublicResource {

    @GET
    @Path("/health")
    @PermitAll  // 인증 없이 접근 가능
    public String health() {
        return "OK";
    }

    @GET
    @Path("/deprecated")
    @DenyAll  // 모든 접근 차단
    public String deprecated() {
        return "이 API는 더 이상 사용할 수 없습니다";
    }
}

프로퍼티 기반 RBAC 설정

어노테이션 대신 properties 파일에서 경로별 접근 제어를 설정할 수도 있습니다.

PROPERTIES
# /api/admin/** 경로는 admin 역할 필요
quarkus.http.auth.policy.admin-policy.roles-allowed=admin
quarkus.http.auth.permission.admin-permission.paths=/api/admin/*
quarkus.http.auth.permission.admin-permission.policy=admin-policy

# /api/user/** 경로는 인증만 필요
quarkus.http.auth.permission.user-permission.paths=/api/user/*
quarkus.http.auth.permission.user-permission.policy=authenticated

# /health, /metrics는 누구나 접근 가능
quarkus.http.auth.permission.public-permission.paths=/health,/metrics
quarkus.http.auth.permission.public-permission.policy=permit

# 그 외 모든 경로는 인증 필요
quarkus.http.auth.permission.default-permission.paths=/*
quarkus.http.auth.permission.default-permission.policy=authenticated

어노테이션 방식과 프로퍼티 방식 중 어떤 걸 써야 할까 고민이 되었는데, 결론은 둘 다 쓰는 게 좋다 입니다. 프로퍼티로 전역 정책을 설정하고, 세밀한 제어가 필요한 엔드포인트에는 어노테이션을 추가하는 패턴이 실무에서 많이 쓰입니다.


Spring Security와의 구조 비교

Spring Security에 익숙한 개발자를 위해 핵심적인 구조 차이를 정리합니다.

Spring Security 방식

JAVA
// Spring Security — FilterChain 기반
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/**").authenticated()
                .anyRequest().permitAll()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtDecoder(jwtDecoder())
                    .jwtAuthenticationConverter(jwtAuthConverter())
                )
            );
        return http.build();
    }
}

Quarkus 방식

PROPERTIES
# Quarkus — 설정 파일 + 어노테이션
quarkus.oidc.auth-server-url=https://keycloak.example.com/realms/my-realm
quarkus.oidc.client-id=my-api

quarkus.http.auth.permission.admin.paths=/admin/*
quarkus.http.auth.permission.admin.policy=admin-policy
quarkus.http.auth.policy.admin-policy.roles-allowed=admin
비교 항목Spring SecurityQuarkus Security
설정 방식Java Config (DSL)properties + 어노테이션
필터 체인직접 커스터마이징 가능익스텐션이 자동 구성
유연성매우 높음 (세밀한 커스터마이징)간결하지만 커스터마이징 제한적
학습 곡선가파름 (FilterChain 이해 필요)완만 (설정 기반)
JWT 디코딩JwtDecoder 빈 직접 설정공개키 위치만 설정하면 자동

Spring Security는 "어떤 상황이든 대응할 수 있는 유연함"이 강점이고, Quarkus Security는 "일반적인 케이스를 설정만으로 빠르게 구현하는 편의성"이 강점입니다. 대부분의 REST API 보안은 Quarkus 방식으로 충분합니다.


DevServices for Keycloak

Quarkus의 DevServices는 개발/테스트 환경에서 Keycloak 컨테이너를 자동으로 띄워줍니다.

PROPERTIES
# DevServices가 자동으로 Keycloak을 시작하려면 이 정도 설정이면 충분
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus

# realm 설정 파일 (자동으로 import)
quarkus.keycloak.devservices.realm-path=quarkus-realm.json

# DevServices 전용 설정
%dev.quarkus.keycloak.devservices.enabled=true
%test.quarkus.keycloak.devservices.enabled=true

quarkus-realm.json에 사용자, 역할, 클라이언트 정보를 미리 정의해두면 quarkus dev를 실행할 때 Keycloak이 자동으로 시작되고 설정이 적용됩니다.

JSON
{
  "realm": "quarkus",
  "enabled": true,
  "users": [
    {
      "username": "admin",
      "enabled": true,
      "credentials": [{"type": "password", "value": "admin"}],
      "realmRoles": ["admin", "user"]
    },
    {
      "username": "user",
      "enabled": true,
      "credentials": [{"type": "password", "value": "user"}],
      "realmRoles": ["user"]
    }
  ],
  "roles": {
    "realm": [
      {"name": "admin"},
      {"name": "user"}
    ]
  },
  "clients": [
    {
      "clientId": "my-api",
      "enabled": true,
      "directAccessGrantsEnabled": true,
      "secret": "test-secret"
    }
  ]
}

Docker만 설치되어 있으면 별도 Keycloak 설치 없이 바로 OIDC 인증 개발을 시작할 수 있습니다. Spring Boot에서 Testcontainers를 써야 하는 부분을 Quarkus는 DevServices로 자동화한 거죠.


실전 예제: JWT + Role 기반 API 보호

지금까지의 내용을 종합한 실전 예제입니다.

JAVA
@Path("/api/orders")
@ApplicationScoped
@Authenticated  // 기본적으로 인증 필요
public class OrderResource {

    @Inject
    SecurityIdentity identity;

    @Inject
    JsonWebToken jwt;

    @GET
    @RolesAllowed({"user", "admin"})
    public List<Order> getMyOrders() {
        String userId = jwt.getClaim("userId");
        return Order.findByUserId(userId);
    }

    @GET
    @Path("/all")
    @RolesAllowed("admin")  // 관리자만
    public List<Order> getAllOrders() {
        return Order.listAll();
    }

    @POST
    @RolesAllowed("user")
    public Response createOrder(OrderRequest request) {
        String userId = jwt.getClaim("userId");
        Order order = Order.create(userId, request);
        return Response.status(201).entity(order).build();
    }

    @DELETE
    @Path("/{id}")
    @RolesAllowed("admin")
    public void cancelOrder(@PathParam("id") Long id) {
        Order order = Order.findById(id);
        if (order == null) {
            throw new NotFoundException("주문을 찾을 수 없습니다");
        }
        order.cancel();
    }
}
PROPERTIES
# application.properties
mp.jwt.verify.publickey.location=publicKey.pem
mp.jwt.verify.issuer=https://my-auth-server.example.com

# 헬스체크와 로그인은 공개
quarkus.http.auth.permission.public.paths=/health,/auth/login
quarkus.http.auth.permission.public.policy=permit

# Swagger UI도 공개 (개발 환경)
%dev.quarkus.http.auth.permission.swagger.paths=/q/*
%dev.quarkus.http.auth.permission.swagger.policy=permit

정리

Quarkus 보안의 핵심을 요약합니다.

  • **아키텍처 **: HttpAuthenticationMechanismIdentityProviderSecurityIdentity의 세 단계 구조
  • JWT: quarkus-smallrye-jwt로 토큰 발급과 검증. @ClaimJsonWebToken으로 클레임 접근
  • OIDC: quarkus-oidc로 Keycloak/Auth0 등 외부 Provider 연동. properties 설정만으로 구현 가능
  • ** 인가 어노테이션 **: @RolesAllowed, @Authenticated, @PermissionsAllowed로 세밀한 접근 제어
  • DevServices: 개발/테스트 시 Keycloak을 자동으로 시작해주어 별도 설치 불필요

Quarkus의 보안은 "설정 중심"으로 설계되어 있어서, Spring Security보다 진입 장벽이 낮습니다. 다만 Spring Security만큼의 극한 커스터마이징이 필요하다면 한계가 있을 수 있습니다. 일반적인 JWT/OIDC 기반 API 인증이라면 Quarkus 방식이 훨씬 간결합니다.

댓글 로딩 중...