Java/인증 및 인가, IAM

[Java/IAM] Spring Boot 환경에서 Keycloak 활용하기 -6 : Keycloak 통신 방법(OIDC, Admin REST Client)

adjh54 2025. 2. 12. 20:00
728x170
해당 글은 Spring Boot 환경에서 Keyclaok을 활용하는 방법으로 OIDC 통신 및 Admin REST Client 통신방법에 대해 알아보는 목적으로 작성하였습니다


 

 

 

💡 [참고] Keycloak 초기 구성에서부터 활용방법에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
분류 주제 URL
Docker Docker Compose를 이용한 Keycloak 환경 구성 및 실행 방법 https://adjh54.tistory.com/644
환경설정 Google Cloud Console OAuth 2.0 API 액세스 환경 설정하기 https://adjh54.tistory.com/657
     
이해하기 Keycloak 이해하기 -1 : 구성 요소, 인증 처리과정, 주요 기능 https://adjh54.tistory.com/645
이해하기 Keycloak 이해하기 -2 : SAML/OIDC 프로토콜, 인증 흐름(Authentication flow) 종류 https://adjh54.tistory.com/646
이해하기 Keycloak 이해하기 -3 : 기본 환경 구성 및 로그인/로그아웃 구현 https://adjh54.tistory.com/647
이해하기 Keycloak 이해하기 -4 : Keycloak 권한 및 종류 https://adjh54.tistory.com/655
     
구성하기 Spring Boot 환경에서 Keycloak 활용하기 -1 : OIDC 인증 흐름 구현(Standard Flow) https://adjh54.tistory.com/648
구성하기 Spring Boot 환경에서 Keycloak 활용하기 -2 : OIDC 인증 흐름 구현(Direct Access Grants, Implicit Flow) https://adjh54.tistory.com/649
구성하기 Spring Boot 환경에서 Keycloak 활용하기 -3 : OIDC 인증 흐름 구현(Service Accounts Roles) https://adjh54.tistory.com/654
구성하기 Spring Boot 환경에서 Keycloak 활용하기 -4 : Identity providers Social 소셜 로그인 구현(Google) https://adjh54.tistory.com/658
구성하기 Spring Boot 환경에서 Keycloak 활용하기 -5 : 일반 사용자와 소셜 로그인 매핑 https://adjh54.tistory.com/659
구성하기 Spring Boot 환경에서 Keycloak 활용하기 -6 : Keycloak 통신 방법(OIDC, Admin REST Client) https://adjh54.tistory.com/663
     
Github Spring Boot Keycloak 관련 Repository https://github.com/adjh54ir/blog-codes/tree/main/spring-boot-keycloakhttps://github.com/adjh54ir/blog-codes/tree/main/spring-boot-keycloak-sub

 

 

1) Keycloak


💡 Keycloak

- Red Hat에서 개발한 오픈소스 Identity and Access Management(IAM) 솔루션입니다. 현대적인 애플리케이션과 서비스를 위한 인증 및 권한 부여 기능을 제공하는 인증 서버(Authentication Server)의 기능을 수행합니다.
- Keycloack에서는 여러 플랫폼에서 중앙 집중식 인증 서버로 동작을 합니다. 주요한 기능은 서로 다른 도메인에서 실행되는 애플리케이션 간의 SSO를 지원하거나 REST API 기반에 접근제어 토큰에 대한 인증 제공 및 세션 타임아웃, 동시 로그인 제한과 같은 다양한 세션 기능을 담당합니다.

 

 

Keycloak

Single-Sign On Users authenticate with Keycloak rather than individual applications. This means that your applications don't have to deal with login forms, authenticating users, and storing users. Once logged-in to Keycloak, users don't have to login again

www.keycloak.org

 

1. Keycloak 사용목적


기능 설명
중앙 집중식 인증 관리 여러 애플리케이션에 대한 인증을 한 곳에서 관리할 수 있어 보안 정책 적용과 유지보수가 용이합니다.
SSO(Single Sign-On) 지원 사용자가 한 번의 로그인으로 여러 애플리케이션에 접근할 수 있어 사용자 경험이 향상됩니다.
다양한 인증 프로토콜 OpenID Connect, SAML 2.0 등 표준 프로토콜을 지원하여 다양한 시스템과의 통합이 가능합니다.
소셜 로그인 통합 Google, Facebook 등 소셜 로그인을 쉽게 구현할 수 있습니다.
강력한 보안 기능 2단계 인증, 비밀번호 정책, 세션 관리 등 다양한 보안 기능을 제공합니다.
오픈소스 무료로 사용 가능하며, 활발한 커뮤니티 지원과 지속적인 업데이트가 이루어집니다.
확장성 REST API를 통한 통합이 용이하며, 커스터마이징이 가능한 유연한 아키텍처를 제공합니다.

 

 

2) Keycloak API 통신 방법


💡 Keycloak API 통신 방법

- Keycloak 서버와 API 통신을 하는 방법으로는 OIDC 기반의 통신 방법이나 Keycloak Admin REST Client를 이용하는 방법이 있습니다.

- OIDC의 경우는 일반 사용자의 ‘인증 절차’를 통해서 접근 토큰(Access Token)을 발급받습니다. 그리고, 이를 기반으로 API 통신 시 Header에 토큰을 추가하여 Keycloak 서버에 접근이 가능합니다.
- Keycloak Admin REST Client의 경우는 API 통신 시 client_id, client_secret만으로 Keycloak 서버에 접근이 가능합니다. 토큰 발급 과정 없이 API를 호출할 수 있어서 더 간단한 방식으로 관리할 수 있습니다.
구분 인증 방식 사용 목적 주요 기능
OIDC(OpenID Connect) 기반 통신 Access Token 기반 인증 일반 사용자의 인증 및 권한 관리 - 표준화된 엔드포인트를 통한 인증/인가
- 사용자 정보 조회
- 권한 검증
Keycloak Admin REST Client 기반 통신 client_id/secret 기반 직접 접근 관리자 권한이 필요한 시스템 관리 작업 - 사용자 관리
- 그룹 관리
- 역할 관리

 

 

1. OIDC(OpenID Connect) 기반 통신


💡 OIDC(OpenID Connect) 기반 통신

- OIDC는 OAuth 2.0 프로토콜을 기반으로 하는 인증 레이어로, 사용자 인증을 위한 표준 프로토콜입니다.
- 이 프로토콜을 기반으로 사용자는 ‘인증 절차’를 통해서 ‘접근 토큰(Access Token)’을 발급받아서 API 통신 시 Header에 해당 토큰을 추가하여 Keycloak 서버에 접근이 가능합니다.

 

1.1. OIDC 기반 통신 특징


💡 OIDC 기반 통신 특징

- 사용자 인증 후 발급받은 Access Token을 사용하여 API 통신을 수행합니다.
- Authorization 헤더에 Bearer 토큰을 포함하여 요청합니다.
- 표준화된 엔드포인트를 통한 인증 및 인가 처리를 수행합니다. 사용자 정보 조회, 권한 검증 등의 기능 제공합니다.

 

💡 [참고] OIDC 기반의 인가 코드 및 토큰 발급 과정에 대한 API가 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
 

[OpenSource/API] Keycloak OIDC(OpenID Connect) Endpoint API

해당 글에서는 OIDC Keyclaok Endpoint의 종류에 대해 알아보고 사용되는 예시를 확인해 봅니다.     💡 [참고] Keycloak 초기 구성에서부터 활용방법에 대해 궁금하시면 아래의 글을 참고하시면 도

adjh54.tistory.com

 

1.2. OpenFeign을 사용한 OIDC 기반 Keycloak API 호출 예시


💡 OpenFeign을 사용한 OIDC 기반 Keycloak API 호출 예시

- OpenFeign를 통해서 Keycloak과 통신하여서 접근 토큰을 포함하여 API를 호출하고 사용자를 조회하는 예시입니다.
package com.blog.springbootkeycloak.service;

import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

/**
 * Keycloak 통신을 통해 사용자를 관리합니다.
 *
 * @author : leejonghoon
 * @fileName : KeycloakUserService
 * @since : 2025. 2. 10.
 */
@FeignClient(
        name = "keycloak-user-service",
        url = "${keycloak.auth-server-url}/admin/realms/${keycloak.realm}"
)
@Service
public interface KeycloakUserService {

    /**
     * 사용자 정보를 전체 조회합니다.
     *
     * @param bearerToken
     * @return
     */
    @GetMapping("/users")
    List<UserRepresentation> getKeycloakUsers(@RequestHeader("Authorization") String bearerToken);

    /**
     * 사용자 정보 내에 필터링을 포함하여 조회합니다.
     *
     * @param bearerToken
     * @param email
     * @param emailVerified
     * @param enabled
     * @param firstName
     * @param lastName
     * @param q
     * @param search
     * @param username
     * @return
     */
    @GetMapping("/users")
    List<UserRepresentation> getKeycloakFilterUsers(
            @RequestHeader("Authorization") String bearerToken,
            @RequestParam(required = false) String email,
            @RequestParam(required = false) Boolean emailVerified,
            @RequestParam(required = false) Boolean enabled,
            @RequestParam(required = false) String firstName,
            @RequestParam(required = false) String lastName,
            @RequestParam(required = false) String q,
            @RequestParam(required = false) String search,
            @RequestParam(required = false) String username
    );
}

 

 

2. Keycloak Admin REST Client 기반 통신


💡 Keycloak Admin REST Client 기반 통신

- Keycloak 서버를 프로그래밍 방식으로 관리할 수 있는 Java 기반의 공식 라이브러리입니다.
- client_id와 client_secret만으로 Keycloak 서버에 직접 접근이 가능하여 별도의 토큰 발급 과정이 필요하지 않습니다.
- 사용자 관리, 그룹 관리, 역할 관리 등 Keycloak의 관리자 기능을 API로 제공합니다.
- 주로 관리자 권한이 필요한 작업을 수행할 때 사용됩니다.
 

Keycloak admin client - Keycloak

Keycloak admin client Using the Keycloak admin client to access the Keycloak Admin REST API The Keycloak admin client is a Java library that facilitates the access and usage of the Keycloak Admin REST API. To use it from your application add a dependency o

www.keycloak.org

 

2.1. 의존성 추가


💡 의존성 추가
dependencies {
    // [OpenSource]
    implementation 'org.keycloak:keycloak-admin-client:26.0.4'
}

Maven Repository: org.keycloak » keycloak-admin-client

 

 

2.2. KeycloakConfig


 💡 KeycloakConfig

- Keycloak Admin Client를 설정하는 구성 클래스입니다. Keycloak 클래스를 이용할 때, 아래와 같은 설정 정보를 입력하였기에 공통적인 Keycloak 설정으로 접근이 가능합니다.
package com.blog.springbootkeycloak.config;

import com.blog.springbootkeycloak.config.properties.KeycloakProperties;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 공통으로 사용하는 Keycloak 인스턴스를 구성합니다.
 *
 * @author : jonghoon
 * @fileName : KeycloakConfig
 * @since : 25. 1. 25.
 */
@Configuration
@RequiredArgsConstructor
public class KeycloakConfig {

    final private KeycloakProperties properties;

    @Bean
    public Keycloak keycloak() {
        return KeycloakBuilder.builder()
                .serverUrl(properties.getAuthServerUrl())
                .realm(properties.getRealm())
                .clientId(properties.getResource())
                .clientSecret(properties.getCredentials().getSecret())
                .grantType(OAuth2Constants.CLIENT_CREDENTIALS)
                .build();
    }
}

 

💡 [참고] 해당 정보는 properties 객체화를 시켜서 호출하였고, yaml 파일 형태로 아래와 같은 형태를 가지고 있습니다.
keycloak:
  auth-server-url: <http://localhost:9001>
  realm: dev-realm
  resource: spring-boot-app
  redirect-url: http://localhost:8080/api/v1/keycloak/callback
  credentials:
    secret: WnmQEGAOzEJ7Kyr2UCJha5AXKCjwnGpB

 

 

 

💡 [참고] Properties 객체화 방법
 

[Java] Spring Boot Configuration Processor 활용하기 : 외부 설정 파일(yaml/yml, properties) 불러오기

해당 글에서는 Spring Boot Configuration Processor를 활용하여서 외부 설정 소스 파일(yaml/yml, properties)을 불러오는 다양한 방법에 대해 알아봅니다  1) spring-boot-configuration-processor💡 spring-boot-configuration-

adjh54.tistory.com

 

2.3. KeycloakAdminClientService / KeycloakAdminClientServiceImpl


💡 KeycloakAdminClientService / KeycloakAdminClientServiceImpl

- Keycloak Admin Client 방식을 사용하고 있어, 별도의 토큰 발급 없이 client_id와 client_secret만으로 Keycloak 서버에 직접 접근이 가능합니다.
package com.blog.springbootkeycloak.service.impl;

import com.blog.springbootkeycloak.config.properties.KeycloakProperties;
import com.blog.springbootkeycloak.service.KeycloakAdminClientService;
import lombok.RequiredArgsConstructor;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * keycloak Admin Client 기반 사용자 관리
 *
 * @author : leejonghoon
 * @fileName : KeycloakAdminClientServiceImpl
 * @since : 2025. 2. 10.
 */
@Service
@RequiredArgsConstructor
public class KeycloakAdminClientServiceImpl implements KeycloakAdminClientService {

    private final Keycloak keycloak;
    private final KeycloakProperties keycloakProperties;

    /**
     * 사용자 정보를 조회합니다.
     *
     * @return
     */
    @Override
    public List<UserRepresentation> getKeycloakUsers() {
        return keycloak
                .realm(keycloakProperties.getRealm())
                .users()
                .list();
    }
}

 

 

3) OIDC 기반 통신 : 사전 구성


 

1. 테스트를 위해 접근 토큰의 세션 시간을 임시로 늘려줍니다.


 💡 테스트를 위해 접근 토큰의 세션 시간을 임시로 늘려줍니다.

- 테스트를 하다 보면 짧은 토큰 시간에 따라 매번 토큰을 발급받아야 하는 문제가 있기에, 임시로 토큰 시간을 늘려줍니다.
- Realm settings > Tokens > Access tokens > Access Token Lifespan을 선택하여 1분마다 발급해야 하는 토큰 시간을 테스트를 위해 늘려줍니다.

 

 

2. 사용자 생성


💡 사용자 생성

- 위에 대한 접근이 가능한 Role을 모두 부여하기 위한 사용자를 생성합니다.

 

3. 비밀번호 지정


💡 비밀번호 지정

- Users > Credentials > Set Password를 선택하여 비밀번호를 지정합니다.
- Direct Access Grants 방식을 통해서 토큰을 발급받아올 예정이기에 비밀번호까지 지정합니다.

 

 

💡 [참고] Direct Access Grants 방식에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
 

[Java/IAM] Spring Boot 환경에서 Keycloak 활용하기 -2 : OIDC 인증 흐름 구현(Direct Access Grants, Implicit Flow)

해당 글에서는 Spring Boot 환경에서 Keycloak과의 연동을 통하여 OIDC 인증 흐름 구현(Direct Access Grants, Implicit Flow) 하는 방법에 대해 알아봅니다. 💡 [참고] Keycloak 초기 구성에서부터 활용방법에

adjh54.tistory.com

 

4. 역할 지정


💡 역할 지정

- Users > Role mapping > Assgin role을 선택하여 역할을 부여합니다.

 

 💡 테스트를 위해 모든 권한에 대해서 선택을 하였습니다.

 

 

 

5. 토큰 발급


💡 토큰 발급

- 생성 토큰의 경우는 인증 흐름(Authentication flow) 중 Direct Access Grants 방식을 이용하여서 접근 토큰(Access Token)을 지정합니다.
[POST] http://localhost:9001/realms/dev-realm/protocol/openid-connect/token

curl --location 'http://localhost:9001/realms/dev-realm/protocol/openid-connect/token' \\
--header 'Content-Type: application/x-www-form-urlencoded' \\
--data-urlencode 'grant_type=password' \\
--data-urlencode 'client_id=spring-boot-app' \\
--data-urlencode 'client_secret=WnmQEGAOzEJ7Kyr2UCJha5AXKCjwnGpB' \\
--data-urlencode 'username=subAdmin' \\
--data-urlencode 'password=qwer1234'

 

 

 

4) OIDC 기반 통신 : 실행 코드


💡 OIDC 기반 통신 -1 : 실행 코드

- OIDC 기반의 통신 처리이기에 접근 토큰(AccessToken)을 발급받아서 이를 기반으로 Keycloak 서버에 리소스를 요청하여 처리를 수행합니다.

 

1. 기능 설명


💡 기능 설명

- 해당 기능은 Keycloak의 사용자를 다루기 위한 Endpoint를 관리하고 있습니다.
- RESTful 아키텍처 원칙을 따르는 표준 HTTP 메서드 사용하며 JSON 형식의 요청/응답 데이터 구조를 가지고 있습니다.
- Bearer 토큰 기반의 인증 방식이며 페이지네이션과 필터링을 통한 효율적인 데이터 조회를 수행합니다.
HTTP Method Endpoint 설명
GET /admin/realms/{realm}/users 사용자 목록 조회
POST /admin/realms/{realm}/users 새로운 사용자 생성
GET /admin/realms/{realm}/users/{id} 특정 사용자 정보 조회
PUT /admin/realms/{realm}/users/{id} 사용자 정보 업데이트
DELETE /admin/realms/{realm}/users/{id} 사용자 삭제
PUT /admin/realms/{realm}/users/{id}/reset-password 사용자 비밀번호 재설정

 

 

Keycloak Admin REST API

POST /admin/realms/{realm}/clients/{client-uuid}/certificates/{attr}/download Get a keystore file for the client, containing private key and public certificate Parameters Path Parameters Name Description Default Pattern realm required realm name (not id!)

www.keycloak.org

 

2. KeycloakUserFeignClient : Keycloak 통신


 💡 KeycloakUserFeignClient : Keycloak 통신

- 해당 인터페이스의 경우는 OpenFeign를 이용하여서 Keycloak 서버와 통신하여 요청/응답 값을 처리하는 부분입니다.
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.MappingsRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * Keycloak 서버와 통신하는 OpenFeign
 *
 * @author : leejonghoon
 * @fileName : KeycloakUserFeignClient
 * @since : 2025. 2. 10.
 */
@FeignClient(
        name = "keycloak-user-service",
        url = "${keycloak.auth-server-url}/admin/realms/${keycloak.realm}"
)
@Service
public interface KeycloakUserFeignClient {

    /**
     * 전체 조회 및 필터링
     *
     * @param bearerToken
     * @return
     */
    @GetMapping("/users")
    List<UserRepresentation> selectKeycloakUserDetail(
            @RequestHeader("Authorization") String bearerToken,
            @RequestParam(value = "first", required = false) Integer first,
            @RequestParam(value = "max", required = false) Integer max,
            @RequestParam(value = "search", required = false) String search,
            @RequestParam(value = "username", required = false) String username,
            @RequestParam(value = "email", required = false) String email,
            @RequestParam(value = "enabled", required = false) Boolean enabled,
            @RequestParam(value = "exact", required = false) Boolean exact
    );

    /**
     * 아이디 기반 특정 사용자 조회
     */
    @GetMapping("/{id}}")
    void selectKeycloakUserDetail(
            @RequestHeader("Authorization") String bearerToken,
            @PathVariable String id
    );

    /**
     * 사용자 등록
     */
    @PostMapping("/users")
    void createUser(
            @RequestHeader("Authorization") String bearerToken,
            @RequestBody UserRepresentation userRepresentation
    );

    /**
     * 사용자 수정
     */
    @PutMapping("/users/{id}")
    void updateUser(
            @RequestHeader("Authorization") String bearerToken,
            @PathVariable String id,
            @RequestBody UserRepresentation userRepresentation
    );

    /**
     * 사용자 삭제
     */
    @DeleteMapping("/users/{id}")
    void deleteUser(
            @RequestHeader("Authorization") String bearerToken,
            @PathVariable String id
    );

    /**
     * 비밀번호 재설정
     *
     * @param bearerToken
     * @param credentialRepresentation
     */
    @PutMapping("/users/{id}/reset-password")
    void resetPassword(
            @RequestHeader("Authorization") String bearerToken,
            @PathVariable String id,
            @RequestBody CredentialRepresentation credentialRepresentation
    );
}

 

 

3. KeycloakUserService


💡 KeycloakUserService

- OpenFeign로 구성된 인터페이스를 호출하여 비즈니스 로직 처리를 수행하며 데이터를 재 가공하여 반환하는 역할을 수행합니다.
- 해당 서비스의 주요한 특징은 클라이언트에서 user id 값을 찾기가 어렵기에, 아이디 기반의 조회를 수행하도록 구성하였습니다. - 또한, 기본적으로 전달되는 Bearer 토큰에 대해서 유효성 검증을 수행한 뒤 비즈니스 로직을 처리합니다.
메서드 종류 메서드 명 설명
Public 메서드 selectKeycloakUserList() Keycloak 사용자를 전체 조회하는 메서드입니다. 토큰 유효성을 체크한 후 사용자 목록을 반환합니다.
Public 메서드 createUser() 새로운 사용자를 등록하는 메서드입니다. 토큰 유효성 검사 후 사용자를 생성하며, 중복 사용자, 잘못된 요청 등의 예외를 처리합니다.
Public 메서드 updateUser() 사용자 정보를 수정하는 메서드입니다. username을 기반으로 ID를 조회한 후 사용자 정보를 업데이트합니다.
Public 메서드 deleteUser() 사용자를 삭제하는 메서드입니다. username으로 ID를 조회한 후 해당 사용자를 삭제합니다.
Public 메서드 resetPassword() 사용자의 비밀번호를 재설정하는 메서드입니다. username으로 ID를 조회한 후 새로운 비밀번호로 설정합니다.
Private 메서드 getKeycloakUserId() username을 기반으로 Keycloak 사용자 ID를 조회하는 메서드입니다.
Private 메서드 validateToken() Bearer 토큰의 유효성을 검사하는 메서드입니다.
Private 메서드 isValidToken() 실제 토큰 유효성 검사를 수행하는 메서드로, Keycloak 서버와 통신하여 토큰의 유효성을 확인합니다.
package com.blog.springbootkeycloak.service;

import com.blog.springbootkeycloak.config.properties.KeycloakProperties;
import com.blog.springbootkeycloak.dto.*;
import com.blog.springbootkeycloak.service.feign.KeycloakAuthFeignClient;
import com.blog.springbootkeycloak.service.feign.KeycloakUserFeignClient;
import feign.FeignException;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.MappingsRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;

import java.util.List;

/**
 * Keycloak 비즈니스 관리를 수행합니다.
 *
 * @author : leejonghoon
 * @fileName : KeycloakUserService
 * @since : 2025. 2. 10.
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class KeycloakUserService {

    private final KeycloakProperties properties;                    // Keycloak 설정 정보
    private final KeycloakUserFeignClient keycloakUserFeignClient;  // Keycloak User 통신
    private final KeycloakAuthFeignClient keycloakAuthFeignClient;  // Keycloak Auth 통신

    /**
     * Keycloak 사용자를 전체 조회합니다.
     *
     * @param bearerToken
     * @return
     */
    public List<UserRepresentation> selectKeycloakUserList(String bearerToken, KeycloakUserSearchDto kus) {

        // 1. [Keycloak] 토큰 유효성 체크
        this.validateToken(bearerToken);

        // 2. [Keycloak] 사용자 조회
        return keycloakUserFeignClient.selectKeycloakUserDetail(
                bearerToken,
                kus.getFirst(),
                kus.getMax(),
                kus.getSearch(),
                kus.getUsername(),
                kus.getEmail(),
                kus.getEnabled(),
                true
        );
    }

    /**
     * 사용자 등록
     *
     * @param bearerToken
     * @param ur
     */
    public int createUser(String bearerToken, UserRepresentation ur) {
        int result = 0;

        // 1. [Keycloak] 토큰 유효성 체크
        this.validateToken(bearerToken);

        try {
            // 2. [Keycloak] 사용자 생성
            keycloakUserFeignClient.createUser(bearerToken, ur);
            result = 1;
        } catch (FeignException.Conflict e) {
            throw new IllegalStateException("이미 존재하는 사용자입니다: " + e.getMessage());
        } catch (FeignException.BadRequest e) {
            throw new IllegalArgumentException("잘못된 사용자 정보입니다: " + e.getMessage());
        } catch (FeignException.Unauthorized | FeignException.Forbidden e) {
            throw new SecurityException("인증/인가 오류가 발생했습니다: " + e.getMessage());
        } catch (Exception e) {
            throw new RuntimeException("사용자 생성 중 예기치 않은 오류가 발생했습니다: " + e.getMessage());
        }
        return result;
    }

    /**
     * 사용자 수정
     *
     * @param bearerToken
     * @param ur
     */
    public int updateUser(String bearerToken, UserRepresentation ur) {
        int result = 0;

        // 1. [Keycloak] 토큰 유효성 체크
        this.validateToken(bearerToken);

        // 2. [Keycloak] username 기반 ID 조회
        String id = this.getKeycloakUserId(bearerToken, ur.getUsername());

        try {
            // 3. [Keycloak] 사용자 수정
            keycloakUserFeignClient.updateUser(bearerToken, id, ur);
            result = 1;
        } catch (FeignException.NotFound e) {
            throw new IllegalArgumentException("존재하지 않는 사용자입니다: " + id);
        } catch (FeignException.BadRequest e) {
            throw new IllegalArgumentException("잘못된 사용자 정보입니다: " + e.getMessage());
        } catch (FeignException.Unauthorized | FeignException.Forbidden e) {
            throw new SecurityException("인증/인가 오류가 발생했습니다: " + e.getMessage());
        } catch (Exception e) {
            throw new RuntimeException("사용자 정보 수정 중 예기치 않은 오류가 발생했습니다: " + e.getMessage());
        }
        return result;
    }

    /**
     * 사용자 삭제
     *
     * @param bearerToken
     * @param bearerToken
     * @param ur
     */
    public int deleteUser(String bearerToken, UserRepresentation ur) {
        int result = 0;
        // 1. [Keycloak] 토큰 유효성 체크
        this.validateToken(bearerToken);

        // 2. [Keycloak] username 기반 ID 조회
        String id = this.getKeycloakUserId(bearerToken, ur.getUsername());

        try {
            // 3. [Keycloak] 사용자 삭제
            keycloakUserFeignClient.deleteUser(bearerToken, id);
            result = 1;
        } catch (FeignException.NotFound e) {
            throw new IllegalArgumentException("존재하지 않는 사용자입니다: " + id);
        } catch (FeignException.Unauthorized | FeignException.Forbidden e) {
            throw new SecurityException("인증/인가 오류가 발생했습니다: " + e.getMessage());
        } catch (Exception e) {
            throw new RuntimeException("사용자 삭제 중 예기치 않은 오류가 발생했습니다: " + e.getMessage());
        }
        return result;
    }

    /**
     * 비밀번호를 재설정합니다.
     *
     * @param bearerToken
     * @param keycloakUserResetPwDto
     * @return
     */
    public int resetPassword(String bearerToken, KeycloakUserResetPwDto keycloakUserResetPwDto) {
        int intResult = 0;

        // 1. [Keycloak] 토큰 유효성 체크
        this.validateToken(bearerToken);

        // 2. [Keycloak] username 기반 ID 조회
        String id = this.getKeycloakUserId(bearerToken, keycloakUserResetPwDto.getUsername());

        // 3. [Keycloak] username 기반의 사용자 조회
        CredentialRepresentation result = new CredentialRepresentation();
        result.setValue(keycloakUserResetPwDto.getValue());
        result.setType("password");
        result.setTemporary(false);
        result.setId(id);

        try {
            // 3. [Keycloak] 비밀번호 재설정
            keycloakUserFeignClient.resetPassword(bearerToken, id, result);
            intResult = 1;
        } catch (FeignException e) {
            // 기타 Feign 예외
            throw new RuntimeException("Failed to reset password: " + e.getMessage());
        }

        return intResult;

    }

    // ***********************************************************************************************************************************
    // *************************************************** private Method ****************************************************************
    // ***********************************************************************************************************************************

    /**
     * username 기반 id 조회
     *
     * @param bearerToken
     * @param username
     * @return
     */
    private String getKeycloakUserId(String bearerToken, String username) {

        // [Validation] 빈 값 체크
        if (username == null) {
            throw new IllegalArgumentException("username이 존재하지 않습니다.: ");
        }

        // [Validation] 빈 값 체크
        if (username.isEmpty()) {
            throw new IllegalArgumentException("username이 존재하지 않습니다.: ");
        }

        // [Keycloak] username 기반의 사용자 조회
        List<UserRepresentation> result = keycloakUserFeignClient.selectKeycloakUserDetail(
                bearerToken,
                null,
                null,
                null,
                username,
                null,
                null,
                true
        );

        // [Validation] 유효하지 않은 사용자
        if (result.isEmpty()) {
            throw new IllegalArgumentException("해당 username으로 등록된 사용자를 찾을 수 없습니다: " + username);
        }

        return result.get(0).getId();
    }

    /**
     * 토큰 유효성을 검사하고 예외를 발생시킵니다.
     *
     * @param bearerToken
     */
    private void validateToken(String bearerToken) {
        log.debug("bearerToken :: {}", bearerToken);
        if (!isValidToken(bearerToken)) {
            log.error("토큰이 유효하지 않습니다.");
            throw new IllegalArgumentException("토큰이 유효하지 않습니다.");
        }
    }

    /**
     * 토큰이 유효한지 체크를 수행합니다.
     *
     * @param bearerToken
     * @return
     */
    private boolean isValidToken(String bearerToken) {

        String accessToken = bearerToken.split(" ")[1];     // Bearer를 제외하고 토큰 값만 전달
        TokenIntrospectionReqDto tokenIntrospectionReqDto = TokenIntrospectionReqDto.builder()
                .token(accessToken)
                .client_id(properties.getResource())
                .client_secret(properties.getCredentials().getSecret())
                .build();
        TokenIntrospectionResDto validTokenDto = keycloakAuthFeignClient.tokenIntrospect(tokenIntrospectionReqDto);      // Keycloak : 토큰 유효성 검증
        return validTokenDto.getActive();
    }

}

 

 

4. KeycloakUserController


💡 KeycloakUserController

- 구성한 서비스를 호출하여서 최종적으로 클라이언트에게 반환해 주는 Controller입니다.
HTTP Method Endpoint 설명
POST /api/v1/user/users 사용자 목록 조회
POST /api/v1/user/user 신규 사용자 등록
PUT /api/v1/user/user 기존 사용자 정보 수정
DELETE /api/v1/user/user 사용자 삭제
GET /api/v1/user/change-password 사용자 비밀번호 변경
package com.blog.springbootkeycloak.controller;

import com.blog.springbootkeycloak.dto.KeycloakUserResetPwDto;
import com.blog.springbootkeycloak.dto.KeycloakUserSearchDto;
import com.blog.springbootkeycloak.service.KeycloakUserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.MappingsRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * Keycloak 사용자 엔드포인트
 *
 * @author : leejonghoon
 * @fileName : KeycloakUserController
 * @since : 2025. 2. 10.
 */
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/user")
public class KeycloakUserController {

    private final KeycloakUserService userService;

    /**
     * 사용자 목록을 조회합니다.
     *
     * @param bearerToken
     * @return
     */
    @PostMapping("/users")
    public ResponseEntity<Object> selectKeycloakUsers(
            @RequestHeader("Authorization") String bearerToken,
            @RequestBody KeycloakUserSearchDto keycloakUserSearchDto
    ) {
        List<UserRepresentation> result = userService.selectKeycloakUserList(bearerToken, keycloakUserSearchDto);
        return new ResponseEntity<>(result, HttpStatus.OK);
    }

    /**
     * 사용자 등록
     *
     * @param bearerToken
     * @param userRepresentation
     * @return
     */
    @PostMapping("/user")
    public ResponseEntity<Integer> createUser(
            @RequestHeader("Authorization") String bearerToken,
            @RequestBody UserRepresentation userRepresentation
    ) {
        int result = userService.createUser(bearerToken, userRepresentation);
        return new ResponseEntity<>(result, HttpStatus.OK);
    }

    /**
     * 사용자 수정
     *
     * @param bearerToken
     * @param userRepresentation
     * @return
     */
    @PutMapping("/user")
    public ResponseEntity<Integer> updateUser(
            @RequestHeader("Authorization") String bearerToken,
            @RequestBody UserRepresentation userRepresentation
    ) {
        int result = userService.updateUser(bearerToken, userRepresentation);
        return new ResponseEntity<>(result, HttpStatus.OK);
    }

    /**
     * 사용자 삭제
     *
     * @param bearerToken
     * @param userRepresentation
     * @return
     */
    @DeleteMapping("/user")
    public ResponseEntity<Integer> deleteUser(
            @RequestHeader("Authorization") String bearerToken,
            @RequestBody UserRepresentation userRepresentation
    ) {
        int result = userService.deleteUser(bearerToken, userRepresentation);
        return new ResponseEntity<>(result, HttpStatus.OK);
    }

    /**
     * 비밀번호 변경
     *
     * @param bearerToken
     * @param keycloakUserResetPwDto
     * @return
     */
    @GetMapping("/change-password")
    public ResponseEntity<Integer> changePassword(
            @RequestHeader("Authorization") String bearerToken,
            @RequestBody KeycloakUserResetPwDto keycloakUserResetPwDto
    ) {
        int result = userService.resetPassword(bearerToken, keycloakUserResetPwDto);
        return new ResponseEntity<>(result, HttpStatus.OK);
    }

}

 

 

 

 

 

💡 [참고] 해당 소스코드는 아래의 Github Repository 내에서 확인하실 수 있습니다.
 

blog-codes/spring-boot-keycloak at main · adjh54ir/blog-codes

Contributor9 티스토리 블로그 내에서 활용한 내용들을 담은 레포지토리입니다. Contribute to adjh54ir/blog-codes development by creating an account on GitHub.

github.com

 

 

 

오늘도 감사합니다 😀

그리드형