1. 소개

이 자습서에서는 Keycloak에 사용자 지정 공급자를 추가하는 방법을 보여줍니다. 기존 및 / 또는 비표준 사용자 저장소에서 사용할 수 있도록 인기있는 오픈 소스 ID 관리 솔루션 인 .

2. Keycloak을 사용하는 사용자 지정 공급자 개요

기본 제공되는 Keycloak은 SAML, OpenID Connect 및 OAuth2 와 같은 프로토콜을 기반으로 다양한 표준 기반 통합을 제공합니다 . 이 내장 기능은 매우 강력하지만 때로는 충분하지 않습니다 . 특히 레거시 시스템이 관련된 경우 일반적인 요구 사항은 해당 시스템의 사용자를 Keycloak에 통합하는 것입니다. 이와 유사한 통합 시나리오를 수용하기 위해 Keycloak은 사용자 지정 공급자 개념을 지원합니다.

맞춤형 공급자는 Keycloak의 아키텍처에서 핵심적인 역할을합니다. 로그인 흐름, 인증, 권한 부여와 같은 모든 주요 기능에 해당하는 서비스 공급자 인터페이스가 있습니다. 이 접근 방식을 사용하면 이러한 서비스에 대한 사용자 정의 구현을 연결할 수 있으며 Keycloak은 자체적으로 사용할 것입니다.

2.1. 사용자 지정 공급자 배포 및 검색

가장 단순한 형태의 사용자 지정 공급자는 하나 이상의 서비스 구현을 포함하는 표준 jar 파일입니다. 시작할 때 Keycloak은 클래스 경로를 스캔하고 표준 java.util.ServiceLoader 메커니즘을 사용하여 사용 가능한 모든 공급자를 선택합니다 . 즉, 우리가해야 할 일은 META-INF / services 에서 제공하려는 특정 서비스 인터페이스 이름을 따서 명명 된 파일을 만드는 것입니다. jar 폴더에 구현의 정규화 된 이름을 넣는 것입니다.

하지만 Keycloak에는 어떤 서비스를 추가 할 수 있습니까? Keycloak의 관리 콘솔에서 사용할 수 있는 서버 정보 페이지 로 이동  하면 많은 정보를 볼 수 있습니다.

이 그림에서 왼쪽 열은 주어진 서비스 공급자 인터페이스 (SPI, 줄여서)에 해당하고 오른쪽 열은 특정 SPI에 대해 사용 가능한 공급자를 보여줍니다.

2.2. 사용 가능한 SPI

Keycloak의 주요 문서에는 다음 SPI가 나열되어 있습니다.

  • org.keycloak.authentication.AuthenticatorFactory : 사용자 또는 클라이언트 애플리케이션을 인증하는 데 필요한 작업 및 상호 작용 흐름을 정의합니다.
  • org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory : / auth / realms / master / login-actions / action-token 엔드 포인트 에 도달했을 때 Keycloak이 수행 할 사용자 지정 작업을 생성 할 수 있습니다 . 예를 들어,이 메커니즘은 표준 암호 재설정 흐름 뒤에 있습니다. 이메일에 포함 된 링크에는 이러한 조치 토큰이 포함되어 있습니다.
  • org.keycloak.events.EventListenerProviderFactory : Keycloak 이벤트를 수신하는 프로 바이더를 생성합니다. 이벤트 유형의 자바 독 페이지는 사용자 지정 공급자가 처리 할 수있는 가능한 이벤트 List이 포함되어 있습니다. 이 SPI를 사용하는 일반적인 용도는 감사 데이터베이스를 만드는 것입니다.
  • org.keycloak.adapters.saml.RoleMappingsProvider : 외부 ID 공급자로부터받은 SAML 역할을 Keycloak의 역할에 매핑합니다. 이 매핑은 매우 유연하여 주어진 Realm의 컨텍스트에서 역할의 이름을 변경, 제거 및 / 또는 추가 할 수 있습니다.
  • org.keycloak.storage.UserStorageProviderFactory : Keycloak이 사용자 정의 사용자 저장소에 액세스하도록 허용합니다.
  • org.keycloak.vault.VaultProviderFactory : 커스텀 볼트를 사용하여 Realm 특정 비밀을 저장할 수 있습니다. 여기에는 암호화 키, 데이터베이스 자격 증명 등과 같은 정보가 포함될 수 있습니다.

이제이 List에는 사용 가능한 모든 SPI가 포함되지 않습니다. 가장 잘 문서화되어 있으며 실제로는 사용자 지정이 필요할 가능성이 높습니다.

3. 사용자 지정 공급자 구현

이 기사의 소개에서 언급했듯이 공급자 예제를 통해 읽기 전용 사용자 지정 사용자 저장소와 함께 Keycloak을 사용할 수 있습니다. 예를 들어, 우리의 경우이 사용자 저장소는 몇 가지 속성이있는 일반 SQL 테이블 일뿐입니다.

create table if not exists users(
    username varchar(64) not null primary key,
    password varchar(64) not null,
    email varchar(128),
    firstName varchar(128) not null,
    lastName varchar(128) not null,
    birthDate DATE not null
);

이 사용자 지정 사용자 저장소를 지원하려면 UserStorageProviderFactory 를 구현해야 합니다. SPI 이를 기존 Keycloak 인스턴스에 배포해야합니다.

여기서 핵심은 읽기 전용 부분입니다. 즉, 사용자는 자신의 자격 증명을 사용하여 Keycloak에 로그인 할 수 있지만 암호를 포함하여 사용자 지정 저장소의 정보를 변경할 수는 없습니다.그러나 이것은 실제로 양방향 업데이트를 지원하기 때문에 Keycloak 제한이 아닙니다. 기본 제공 LDAP 공급자는이 기능을 지원하는 공급자의 좋은 예입니다.

3.1. 프로젝트 설정

사용자 지정 공급자 프로젝트는 jar 파일을 만드는 일반 Maven 프로젝트입니다. 시간이 많이 걸리는 공급자의 컴파일-배포-재시작주기를 일반 Keycloak 인스턴스로 방지하기 위해 멋진 트릭을 사용할 것입니다. 프로젝트에 Keycloak을 테스트 시간 의존성으로 포함합니다.

SpringBoot 애플리케이션에 Keycloack을 포함하는 방법에 대해서는 이미 다루었 으므로 여기서 수행하는 방법에 대해서는 자세히 설명하지 않습니다 . 이 기술을 채택함으로써 우리는 더 빠른 시작 시간과 핫 리로드 기능을 얻을 수 있으며 더 원활한 개발자 경험을 제공합니다. 여기서는 예제 SpringBoot 애플리케이션을 재사용하여 사용자 지정 공급자에서 직접 테스트를 실행하므로 테스트 의존성으로 추가 할 것입니다.

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-core</artifactId>
    <version>12.0.2</version>
</dependency>

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-server-spi</artifactId>
    <version>12.0.2</version>
</dependency>

<dependency>
    <groupId>com.baeldung</groupId>
    <artifactId>oauth-authorization-server</artifactId>
    <version>0.1.0-SNAPSHOT</version>
    <scope>test</scope>
</dependency>

keycloak-corekeycloak-server-spi에 최신 11 시리즈 버전을 사용하고 있습니다. Keycloak 의존성에 있습니다.

그러나 oauth-authorization-server 의존성은 Baeldung의 Spring Security OAuth 저장소 에서 로컬로 빌드해야 합니다. .

3.2. UserStorageProviderFactory 구현

UserStorageProviderFactory 구현 을 생성하여 공급자를 시작하고 Keycloak 에서 검색 할 수 있도록합니다.

이 인터페이스에는 11 개의 메서드가 포함되어 있지만 그중 두 가지만 구현하면됩니다.

  • getId () : Keycloak이 관리 페이지에 표시 할이 공급자의 고유 식별자를 반환합니다.
  • create () : 실제 Provider 구현을 반환합니다.

Keycloak은 모든 트랜잭션에 대해 create () 메서드를 호출하여  KeycloakSession 및  ComponentModel 을 인수로 전달 합니다. 여기서 트랜잭션은 사용자 저장소에 액세스해야하는 모든 작업을 의미합니다. 가장 좋은 예는 로그인 흐름입니다. 어떤 시점에서 Keycloak은 특정 Realm에 대해 구성된 모든 사용자 저장소를 호출하여 자격 증명을 확인합니다. 따라서 create () 메서드가 항상 호출 되므로이 시점에서 비용이 많이 드는 초기화 작업을 수행하지 않아야합니다 .

즉, 구현은 매우 간단합니다.

public class CustomUserStorageProviderFactory
  implements UserStorageProviderFactory<CustomUserStorageProvider> {
    @Override
    public String getId() {
        return "custom-user-provider";
    }

    @Override
    public CustomUserStorageProvider create(KeycloakSession ksession, ComponentModel model) {
        return new CustomUserStorageProvider(ksession,model);
    }
}

공급자 ID로 "custom-user-provider"선택 했으며 create () 구현은 단순히 UserStorageProvider  구현 의 새 인스턴스를 반환합니다 . 이제 서비스 정의 파일을 생성하고 프로젝트에 추가하는 것을 잊지 말아야합니다. 이 파일의 이름은 org.keycloak.storage.UserStorageProviderFactory 여야하며 최종 jar 의  META-INF / services 폴더에 있어야합니다.

표준 Maven 프로젝트를 사용하고 있으므로 src / main / resources / META-INF / services 폴더에 추가 할 것입니다 .

이 파일의 내용은 SPI 구현의 정규화 된 이름입니다.

# SPI class implementation
com.baeldung.auth.provider.user.CustomUserStorageProviderFactory

3.3. UserStorageProvider 구현

첫눈에 UserStorageProvider는 구현은 우리가 예상 보이지 않습니다. 여기에는 실제 사용자와 관련된 몇 가지 콜백 메서드 만 포함되어 있습니다. 그 이유는 Keycloak은 공급자가 특정 사용자 관리 측면을 지원하는 다른 믹스 인 인터페이스도 구현할 것으로 기대하기 때문입니다.

사용 가능한 인터페이스의 전체 List은 공급자 기능 이라고하는 Keycloak 문서에서 확인할 수 있습니다 간단한 읽기 전용 공급자의 경우 구현해야하는 유일한 인터페이스는  UserLookupProvider 입니다. 조회 기능 만 제공하므로 Keycloak은 필요할 때 내부 데이터베이스로 사용자를 자동으로 가져옵니다. 그러나 원래 사용자의 암호는 인증에 사용되지 않습니다. 이를 위해서는 CredentialInputValidator 도 구현해야합니다 .

마지막으로, 일반적인 요구 사항은 Keycloak의 관리 인터페이스에있는 사용자 정의 스토어에 기존 사용자를 표시하는 기능입니다. 이를 위해서는 또 다른 인터페이스 인 UserQueryProvider를 구현해야합니다 . 이것은 몇 가지 쿼리 메서드를 추가하고 우리 매장의 DAO 역할을합니다.

따라서 이러한 요구 사항이 주어지면 다음과 같이 구현해야합니다.

public class CustomUserStorageProvider implements UserStorageProvider, 
  UserLookupProvider,
  CredentialInputValidator, 
  UserQueryProvider {
  
    // ... private members omitted
    
    public CustomUserStorageProvider(KeycloakSession ksession, ComponentModel model) {
      this.ksession = ksession;
      this.model = model;
    }

    // ... implementation methods for each supported capability
}

생성자에 전달 된 값을 저장하고 있습니다. 나중에 구현에서 어떻게 중요한 역할을하는지 살펴 보겠습니다.

3.4. UserLookupProvider 구현

Keycloak은이 인터페이스의 메소드를 사용하여 ID , 사용자 이름 또는 이메일이 지정된 UserModel 인스턴스  를 복구합니다 . 이 경우 id는이 사용자의 고유 식별자이며 다음과 같이 형식이 지정됩니다. 'f :' unique_id ':' external_id

  • 'f :'는 페더레이션 된 사용자임을 나타내는 고정 접두사입니다.
  • unique_id 는 사용자의 Keycloak ID입니다.
  • external_id 는 지정된 사용자 저장소에서 사용하는 사용자 식별자입니다. 이 경우 사용자 이름 열의 값이 됩니다.

계속해서 getUserByUsername ()으로 시작하여이 인터페이스의 메서드를 구현해 보겠습니다  .

@Override
public UserModel getUserByUsername(String username, RealmModel realm) {
    try ( Connection c = DbUtil.getConnection(this.model)) {
        PreparedStatement st = c.prepareStatement(
          "select " +
          "  username, firstName, lastName, email, birthDate " + 
          "from users " + 
          "where username = ?");
        st.setString(1, username);
        st.execute();
        ResultSet rs = st.getResultSet();
        if ( rs.next()) {
            return mapUser(realm,rs);
        }
        else {
            return null;
        }
    }
    catch(SQLException ex) {
        throw new RuntimeException("Database error:" + ex.getMessage(),ex);
    }
}

예상대로 이것은 제공된 사용자 이름사용하여 정보를 조회 하는 간단한 데이터베이스 쿼리 입니다. 설명이 필요한 두 가지 흥미로운 점이 있습니다. DbUtil.getConnection () 및  mapUser () .

DbUtil는 어떻게 든이 JDBC를 반환하는 도우미 클래스입니다 연결 에 포함 된 정보에서  ComponentModel 우리가 생성자에 인수했다고합니다. 자세한 내용은 나중에 다룰 것입니다.

에 관해서는 mapUser () , 그 작업은 사용자 데이터를 포함하는 데이터베이스 레코드 매핑하는 것이다 UserModel의  인스턴스를. UserModel는 같은 Keycloak 볼 사용자 엔티티를 나타내고, 그것의 속성을 판독하는 방법이있다. 여기에서 사용할 수있는이 인터페이스의 구현은 Keycloak에서 제공 하는 AbstractUserAdapter 클래스를 확장합니다 . 또한 구현에 Builder 내부 클래스를 추가 했으므로 mapUser ()UserModel 인스턴스를 쉽게 만들 수 있습니다.

private UserModel mapUser(RealmModel realm, ResultSet rs) throws SQLException {
    CustomUser user = new CustomUser.Builder(ksession, realm, model, rs.getString("username"))
      .email(rs.getString("email"))
      .firstName(rs.getString("firstName"))
      .lastName(rs.getString("lastName"))
      .birthDate(rs.getDate("birthDate"))
      .build();
    return user;
}

마찬가지로 다른 방법은 기본적으로 위에서 설명한 것과 동일한 패턴을 따르므로 자세히 다루지 않겠습니다. 공급자의 코드를 참조하고 모든 getUserByXXXsearchForUser 메소드를 확인하십시오 .

3.5. 얻기 연결

이제 DbUtil.getConnection () 메서드를 살펴 보겠습니다 .

public class DbUtil {

    public static Connection getConnection(ComponentModel config) throws SQLException{
        String driverClass = config.get(CONFIG_KEY_JDBC_DRIVER);
        try {
            Class.forName(driverClass);
        }
        catch(ClassNotFoundException nfe) {
           // ... error handling omitted
        }
        
        return DriverManager.getConnection(
          config.get(CONFIG_KEY_JDBC_URL),
          config.get(CONFIG_KEY_DB_USERNAME),
          config.get(CONFIG_KEY_DB_PASSWORD));
    }
}

우리는 것을 알 수 있습니다  ComponentModel가 필요한 모든 매개 변수는 만드는 곳입니다. 그러나 Keycloak은 사용자 지정 공급자에 필요한 매개 변수를 어떻게 알 수 있습니까? 이 질문에 답하려면 CustomUserStorageProviderFactory 로 돌아  가야합니다.

3.6. 구성 메타 데이터

CustomUserStorageProviderFactory 의 기본 계약 UserStorageProviderFactory 에는 Keycloak이 구성 속성 메타 데이터를 쿼리하고 할당 된 값의 유효성을 검사 할 수있는 메서드가 포함되어 있습니다 . 이 경우 JDBC 연결을 설정하는 데 필요한 몇 가지 구성 매개 변수를 정의합니다. 이 메타 데이터는 정적이므로 생성자에서 생성하고 getConfigProperties ()  는 간단히 반환합니다.

public class CustomUserStorageProviderFactory
  implements UserStorageProviderFactory<CustomUserStorageProvider> {
    protected final List<ProviderConfigProperty> configMetadata;
    
    public CustomUserStorageProviderFactory() {
        configMetadata = ProviderConfigurationBuilder.create()
          .property()
            .name(CONFIG_KEY_JDBC_DRIVER)
            .label("JDBC Driver Class")
            .type(ProviderConfigProperty.STRING_TYPE)
            .defaultValue("org.h2.Driver")
            .helpText("Fully qualified class name of the JDBC driver")
            .add()
          // ... repeat this for every property (omitted)
          .build();
    }
    // ... other methods omitted
    
    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return configMetadata;
    }

    @Override
    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config)
      throws ComponentValidationException {
       try (Connection c = DbUtil.getConnection(config)) {
           c.createStatement().execute(config.get(CONFIG_KEY_VALIDATION_QUERY));
       }
       catch(Exception ex) {
           throw new ComponentValidationException("Unable to validate database connection",ex);
       }
    }
}

에서  validateConfiguration () , 우리는 우리가 우리의 렐름에 추가 제공시에 전달 된 매개 변수의 유효성을 확인하는 데 필요한 모든 것을 얻을 것이다 . 우리의 경우이 정보를 사용하여 데이터베이스 연결을 설정하고 유효성 검사 쿼리를 실행합니다. 문제가 발생하면 ComponentValidationException을 던져  Keycloak에 매개 변수가 유효하지 않다는 신호를 보냅니다.

또한 여기에 표시되지는 않지만 onCreated () 메서드를 사용하여 관리자가 공급자를 Realm에 추가 할 때마다 실행되는 로직을 첨부 할 수도 있습니다 . 이를 통해 일회성 초기화 시간 로직을 실행하여 특정 시나리오에 필요할 수있는 스토어를 사용할 수 있도록 준비 할 수 있습니다. 예를 들어,이 방법을 사용하여 데이터베이스를 수정하고 주어진 사용자가 이미 Keycloak을 사용했는지 여부를 기록하는 열을 추가 할 수 있습니다.

3.7. CredentialInputValidator 구현

이 인터페이스에는 사용자 자격 증명을 확인하는 메서드가 포함되어 있습니다 . Keycloak 자격 증명 (비밀번호, OTP 토큰, X.509 인증서 등)의 다른 유형을 지원하기 때문에 그것에서 지정된 유형 지원하는 경우, 우리 제공자는 알려야합니다 supportsCredentialType을 ()  하고 주어진 영역의 맥락에서 구성된 isConfiguredFor () .

우리의 경우에는 암호 만 지원하고 추가 구성이 필요하지 않기 때문에 나중 메서드를 전자에 위임 할 수 있습니다.

@Override
public boolean supportsCredentialType(String credentialType) {
    return PasswordCredentialModel.TYPE.endsWith(credentialType);
}

@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
    return supportsCredentialType(credentialType);
}

실제 암호 유효성 검사는 isValid ()  메서드 에서 발생합니다 .

@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) {
    if(!this.supportsCredentialType(credentialInput.getType())) {
        return false;
    }
    StorageId sid = new StorageId(user.getId());
    String username = sid.getExternalId();
    
    try (Connection c = DbUtil.getConnection(this.model)) {
        PreparedStatement st = c.prepareStatement("select password from users where username = ?");
        st.setString(1, username);
        st.execute();
        ResultSet rs = st.getResultSet();
        if ( rs.next()) {
            String pwd = rs.getString(1);
            return pwd.equals(credentialInput.getChallengeResponse());
        }
        else {
            return false;
        }
    }
    catch(SQLException ex) {
        throw new RuntimeException("Database error:" + ex.getMessage(),ex);
    }
}

여기서 논의 할만한 몇 가지 사항이 있습니다. 첫째, 우리는에서 외부 ID를 추출하는 방법을 통지 , UserModel  사용 StorageId의  Keycloak의 ID로 초기화 된 개체를. 이 ID가 잘 알려진 형식을 가지고 있다는 사실을 사용하여 거기에서 사용자 이름을 추출 할 수 있지만 여기서 안전하게 플레이하고이 지식을 Keycloak 제공 클래스에 캡슐화하는 것이 좋습니다.

다음으로 실제 암호 확인이 있습니다. 단순하고 안전하지 않은 데이터베이스의 경우 암호 확인은 간단합니다. 데이터베이스 값을 getChallengeResponse ()를 통해 사용할 수있는 사용자 제공 값과 비교하기 만하면 됩니다. 물론 실제 공급자는 데이터베이스에서 해시 정보 암호 및 솔트 값을 생성하고 해시를 비교하는 것과 같은 몇 가지 단계가 더 필요합니다.

마지막으로, 사용자 저장소에는 일반적으로 최대 사용 기간, 차단 및 / 또는 비활성 상태 등과 같은 암호와 관련된 수명주기가 있습니다. 그럼에도 불구하고 공급자를 구현할 때 isValid () 메서드가이 논리를 추가하는 위치입니다.

3.8. UserQueryProvider 구현

UserQueryProvider의 기능 인터페이스는 우리의 공급자가 상점에서 사용자를 검색 할 수 Keycloak을 알려줍니다. 이 기능을 지원하면 관리 콘솔에서 사용자를 볼 수 있으므로 편리합니다.

이 인터페이스의 메소드에는 상점의 총 사용자 수를 가져 오는 getUsersCount () 와 여러 getXXX ()searchXXX () 메소드가 있습니다. 이 쿼리 인터페이스는 사용자뿐만 아니라 그룹 검색도 지원하지만 이번에는 다루지 않습니다.

이러한 메서드의 구현은 매우 유사하므로 그중 하나 인 searchForUser () 만 살펴 보겠습니다 .

@Override
public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
    try (Connection c = DbUtil.getConnection(this.model)) {
        PreparedStatement st = c.prepareStatement(
          "select " + 
          "  username, firstName, lastName, email, birthDate " +
          "from users " + 
          "where username like ? + 
          "order by username limit ? offset ?");
        st.setString(1, search);
        st.setInt(2, maxResults);
        st.setInt(3, firstResult);
        st.execute();
        ResultSet rs = st.getResultSet();
        List<UserModel> users = new ArrayList<>();
        while(rs.next()) {
            users.add(mapUser(realm,rs));
        }
        return users;
    }
    catch(SQLException ex) {
        throw new RuntimeException("Database error:" + ex.getMessage(),ex);
    }
}

보시다시피 여기에는 특별한 것이 없습니다. 일반 JDBC 코드뿐입니다. 언급 할만한 구현 참고 사항 : UserQueryProvider 메서드는 일반적으로 페이징 및 비 페이징 버전으로 제공됩니다. 사용자 저장소에는 잠재적으로 많은 수의 레코드가있을 수 있으므로 비 페이징 버전은 적절한 기본값을 사용하여 페이징 된 버전에 위임해야합니다. 더 좋은 점은 "적당한 기본값"이 무엇인지 정의하는 구성 매개 변수를 추가 할 수 있다는 것입니다.

4. 테스트

공급자를 구현 했으므로 이제 포함 된 Keycloak 인스턴스를 사용하여 로컬에서 테스트 할 차례입니다. 프로젝트의 코드에는 Keycloak 및 사용자 지정 사용자 데이터베이스를 부트 스트랩 한 다음 1 시간 동안 잠자기 전에 콘솔에 액세스 URL을 인쇄하는 데 사용한 라이브 테스트 클래스가 포함되어 있습니다.

이 설정을 사용하면 브라우저에서 인쇄 된 URL을 열기 만하면 사용자 지정 공급자가 의도 한대로 작동하는지 확인할 수 있습니다.

관리 콘솔에 액세스하려면 application-test.yml 파일 을 확인하여 얻을 수있는 관리자 자격 증명을 사용 합니다. 로그인 한 후 "서버 정보"페이지로 이동합니다.

"공급자"탭에서 다른 내장 스토리지 공급자와 함께 표시되는 사용자 지정 공급자를 볼 수 있습니다.

Baeldung 영역이 이미이 공급자를 사용하고 있는지 확인할 수도 있습니다. 이를 위해 왼쪽 상단 드롭 다운 메뉴에서 선택한 다음 사용자 연합 페이지 로 이동할 수 있습니다 .

Next, let's test an actual login into this realm. We'll use the realm's account management page, where a user can manage its data. Our Live Test will print this URL before going into sleep, so we can just copy it from the console and paste it into the browser's address bar.

The test data contains three users: user1, user2, and user3. The password for all of them is the same: “changeit”. Upon successful login, we'll see the account management page displaying the imported user's data:

However, if we try to modify any data, we'll get an error. This is expected, as our provider is read-only, so Keycloak doesn't allow to modify it. For now, we'll leave it as is since supporting bi-directional synchronization is beyond the scope of this article.

5. Conclusion

이 기사에서는 사용자 스토리지 제공자를 구체적인 예로 사용하여 Keycloak에 대한 사용자 정의 제공자를 생성하는 방법을 보여주었습니다. 예제의 전체 소스 코드는 GitHub 에서 찾을 수 있습니다 .