728x90
Keycloak를 도입하여 인증·인가를 간편하게 구성할 수 있지만, “Admin REST API”로는 사용자와 그룹 정보를 한꺼번에 조회하기가 쉽지 않습니다.
이를 위해 RealmResourceProvider라는 확장(SPI)을 구현해 N+1 문제를 개선하고, 하나의 엔드포인트에서 사용자와 그들의 그룹 목록을 동시에 가져오는 방법을 소개합니다.
문제: 기본 Admin API의 N+1 호출
Keycloak에는
- GET /admin/realms/{realm}/users : 사용자 목록 (그룹 정보 X)
- GET /admin/realms/{realm}/users/{userId}/groups : 해당 사용자의 그룹 목록 엔드포인트만 제공됩니다.
수많은 사용자에 대해 그룹 정보를 확인하려면,
- 사용자 목록 API 호출 → 사용자 IDs 획득
- 사용자마다 그룹 목록 API를 추가 호출(N번)
결과적으로 N+1 문제(호출이 다량 발생)가 생깁니다.
원하는 목표: 단일 API로 (사용자, 그가 속한 그룹들) 정보를 한꺼번에 받기.
해결책: RealmResourceProvider(REST Extension) 활용
Keycloak는 내부 확장(SPI)을 통해, 서버 내부에 사용자 정의 JAX-RS 리소스(REST API)를 추가할 수 있습니다.
이것을 RealmResourceProvider라 부르며, 해당 SPI를 구현해 Keycloak 세션(KeycloakSession)에서 사용자/그룹 목록을 직접 조회한 뒤, 맞춤 JSON 형태로 반환할 수 있습니다.
장점
- N+1 호출을 한 번의 조회 로직으로 처리
- 원하는 응답 DTO(예: username, email, groups 등)로 커스텀 가능
- Keycloak 내부 인증/인가 컨텍스트(session.getContext())를 그대로 활용
단점
- Java(JVM 언어) 로 SPI 구현 필요
- Keycloak 버전업 시 호환성 유지 주의
- 운영 환경에서 직접 배포해야 하므로 DevOps 부담
구현 예시
프로젝트 구조 (Gradle)
my-custom-rest-extension/
├─ build.gradle
├─ settings.gradle
├─ src/
│ └─ main/
│ ├─ java/
│ │ └─ com/example/keycloak/rest/
│ │ ├─ MyRealmResourceProviderFactory.java
│ │ ├─ MyRealmResourceProvider.java
│ │ ├─ MyCustomRestResource.java
│ │ └─ dto/
│ │ └─ UserWithGroupsDto.java
│ └─ resources/
│ └─ META-INF/
│ └─ services/
│ └─ org.keycloak.services.resource.RealmResourceProviderFactory
└─ Dockerfile
- MyRealmResourceProviderFactory: Keycloak가 SPI를 인식하기 위한 팩토리
- MyRealmResourceProvider: JAX-RS 리소스 객체를 리턴해 주는 Provider
- MyCustomRestResource: 실제 REST API 구현 (사용자/그룹 정보를 한 번에)
- UserWithGroupsDto: 반환 DTO 예시
- META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory: Service Loader 정의
- Dockerfile: 결과물을 Keycloak 이미지를 기반으로 빌드
주요 코드
아래는 간단히 “사용자 목록 + 그룹 목록”을 JSON으로 반환하는 엔드포인트 예시입니다.
(“/realms/{realm}/my-custom-rest/users-with-groups” 경로)
@Path("/")
public class MyCustomRestResource {
private final KeycloakSession session;
public MyCustomRestResource(KeycloakSession session) {
this.session = session;
}
@GET
@Path("users-with-groups")
@Produces(MediaType.APPLICATION_JSON)
public Response getUsersWithGroups(
@QueryParam("first") @DefaultValue("0") int first,
@QueryParam("max") @DefaultValue("20") int max,
@QueryParam("search") @DefaultValue("") String search
) {
// 권한 체크 (예: realm-admin 역할 확인)
if (!isCallerRealmAdmin()) {
return Response.status(Response.Status.FORBIDDEN)
.entity("Not authorized")
.build();
}
RealmModel realm = session.getContext().getRealm();
List<UserModel> users;
// 검색어가 있으면 검색
if (!search.isEmpty()) {
users = session.users().searchForUserStream(realm, search, first, max)
.collect(Collectors.toList());
} else {
users = session.users().getUsersStream(realm, first, max)
.collect(Collectors.toList());
}
List<UserWithGroupsDto> results = users.stream().map(user -> {
// 그룹 목록 추출
List<String> groupNames = user.getGroupsStream()
.map(GroupModel::getName)
.collect(Collectors.toList());
return new UserWithGroupsDto(
user.getId(),
user.getUsername(),
user.getEmail(),
groupNames
);
}).collect(Collectors.toList());
return Response.ok(results).build();
}
private boolean isCallerRealmAdmin() {
// Keycloak 내 세션에서 현재 인증 유저의 역할 체크
AuthenticatedUserSession authSession = new AppAuthManager.BearerTokenAuthenticator(session).authenticate();
if (authSession == null) return false;
RealmModel realm = session.getContext().getRealm();
RoleModel adminRole = realm.getRole("realm-admin");
if (adminRole == null) return false;
return authSession.getUser().hasRole(adminRole);
}
}
위 로직은:
- search, first, max 파라미터로 페이징 & 검색
- UserModel 목록을 순회하며 **user.getGroupsStream()*으로 그룹들 조회
- DTO(UserWithGroupsDto)로 변환 후 JSON 배열 반환
Gradle 빌드 & Docker 배포
build.gradle 예시
plugins {
id 'java'
}
group = 'com.example.keycloak'
version = '1.0.0'
repositories {
mavenCentral()
}
ext {
keycloakVersion = '21.1.1'
}
dependencies {
compileOnly "org.keycloak:keycloak-server-spi:${keycloakVersion}"
compileOnly "org.keycloak:keycloak-services:${keycloakVersion}"
}
jar {
archiveBaseName.set("my-custom-rest-extension")
archiveVersion.set("")
}
- compileOnly로 Keycloak 서버 SPI, services 의존성 선언
- jar 태스크로 JAR 생성
Dockerfile
FROM quay.io/keycloak/keycloak:21.1.1
WORKDIR /opt/keycloak
# Gradle 빌드 후 산출된 JAR 복사
COPY build/libs/my-custom-rest-extension.jar /opt/keycloak/providers/
# 개발 모드 실행
ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start-dev"]
728x90
'Keycloak' 카테고리의 다른 글
[Keycloak] 어드민 API 호출 권한, 사용자에게 직접 부여하기 (0) | 2025.04.01 |
---|---|
[Keycloak] Spring Boot Resource Server 연동하기 (0) | 2025.03.26 |
[Keycloak] JWT에 Custom 스코프 추가하기 (0) | 2025.03.26 |