보안 — JWT, OIDC, Keycloak으로 Quarkus 애플리케이션 보호하기
API 하나를 만들어서 배포했는데, 인증 없이 누구나 호출할 수 있다면? 보안은 "나중에 붙이자"가 아니라, 처음부터 구조를 잡아야 하는 영역입니다. Quarkus에서 보안을 어떻게 구성하는지 처음부터 정리해봅니다.
Quarkus Security 아키텍처
Quarkus의 보안은 세 가지 핵심 컴포넌트로 구성됩니다.
한 줄 정의: 요청이 들어오면 HttpAuthenticationMechanism 이 인증 정보를 추출하고, IdentityProvider 가 신원을 확인하며, 결과로 SecurityIdentity 가 생성됩니다.
HTTP 요청
│
▼
┌──────────────────────────┐
│ HttpAuthenticationMechanism │ ← 토큰/쿠키에서 인증 정보 추출
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ IdentityProvider │ ← 자격증명 검증 (DB, OIDC, JWT 등)
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ SecurityIdentity │ ← 인증된 사용자 정보 + 역할/권한
└──────────────────────────┘
│
▼
@RolesAllowed 등으로 인가 체크
이 구조를 Spring Security와 비교하면 이렇습니다.
| Quarkus | Spring Security | 역할 |
|---|---|---|
HttpAuthenticationMechanism | AuthenticationFilter | 인증 정보 추출 |
IdentityProvider | AuthenticationProvider | 자격증명 검증 |
SecurityIdentity | Authentication | 인증 결과 객체 |
| 어노테이션 기반 인가 | SecurityFilterChain + 어노테이션 | 접근 제어 |
Spring Security의 FilterChain은 유연하지만 구조가 복잡합니다. Quarkus는 이 부분을 단순화해서, 대부분의 경우 익스텐션 추가 + properties 설정 + 어노테이션 만으로 보안을 구현할 수 있습니다.
quarkus-smallrye-jwt — JWT 토큰 검증
REST API에서 가장 흔한 인증 방식인 JWT를 먼저 살펴봅니다.
의존성 추가
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>
설정
# 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 토큰 발급
@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으로 클레임에 접근할 수 있습니다.
@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를 사용하는 경우가 많습니다.
의존성 추가
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
Bearer 토큰 검증 (API 서버)
# 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가 자동으로 다음을 처리합니다.
- OIDC Provider의
.well-known/openid-configuration엔드포인트에서 설정 정보를 가져옴 - 공개키(JWKS)를 자동으로 다운로드
- 모든 요청에서 Bearer 토큰을 추출해 검증
- 검증 성공 시
SecurityIdentity를 생성
웹 애플리케이션 (Authorization Code Flow)
웹 애플리케이션에서 OIDC 로그인 플로우를 구현할 때는 application-type을 변경합니다.
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 — 역할 기반 접근 제어
@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 — 인증만 요구
@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부터 지원하는 더 세밀한 권한 제어입니다.
@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
@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 파일에서 경로별 접근 제어를 설정할 수도 있습니다.
# /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 방식
// 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 방식
# 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 Security | Quarkus Security |
|---|---|---|
| 설정 방식 | Java Config (DSL) | properties + 어노테이션 |
| 필터 체인 | 직접 커스터마이징 가능 | 익스텐션이 자동 구성 |
| 유연성 | 매우 높음 (세밀한 커스터마이징) | 간결하지만 커스터마이징 제한적 |
| 학습 곡선 | 가파름 (FilterChain 이해 필요) | 완만 (설정 기반) |
| JWT 디코딩 | JwtDecoder 빈 직접 설정 | 공개키 위치만 설정하면 자동 |
Spring Security는 "어떤 상황이든 대응할 수 있는 유연함"이 강점이고, Quarkus Security는 "일반적인 케이스를 설정만으로 빠르게 구현하는 편의성"이 강점입니다. 대부분의 REST API 보안은 Quarkus 방식으로 충분합니다.
DevServices for Keycloak
Quarkus의 DevServices는 개발/테스트 환경에서 Keycloak 컨테이너를 자동으로 띄워줍니다.
# 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이 자동으로 시작되고 설정이 적용됩니다.
{
"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 보호
지금까지의 내용을 종합한 실전 예제입니다.
@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();
}
}
# 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 보안의 핵심을 요약합니다.
- **아키텍처 **:
HttpAuthenticationMechanism→IdentityProvider→SecurityIdentity의 세 단계 구조 - JWT:
quarkus-smallrye-jwt로 토큰 발급과 검증.@Claim과JsonWebToken으로 클레임 접근 - OIDC:
quarkus-oidc로 Keycloak/Auth0 등 외부 Provider 연동. properties 설정만으로 구현 가능 - ** 인가 어노테이션 **:
@RolesAllowed,@Authenticated,@PermissionsAllowed로 세밀한 접근 제어 - DevServices: 개발/테스트 시 Keycloak을 자동으로 시작해주어 별도 설치 불필요
Quarkus의 보안은 "설정 중심"으로 설계되어 있어서, Spring Security보다 진입 장벽이 낮습니다. 다만 Spring Security만큼의 극한 커스터마이징이 필요하다면 한계가 있을 수 있습니다. 일반적인 JWT/OIDC 기반 API 인증이라면 Quarkus 방식이 훨씬 간결합니다.