1. 개요

Keycloak은 RedHat에서 관리하고 JBoss에서 Java로 개발 한 오픈 소스 ID 및 액세스 관리 솔루션 입니다.

이 예제에서는 Spring Boot 애플리케이션에 내장된 Keycloak 서버를 설정하는 방법을 배웁니다 . 이를 통해 사전 구성된 Keycloak 서버를 쉽게 시작할 수 있습니다.

Keycloak은 독립 실행형 서버 로 실행할 수도 있지만 Admin Console을 통해 다운로드하고 설정해야 합니다.

2. Keycloak 사전 구성

먼저 Keycloak 서버를 사전 구성하는 방법을 이해합시다.

서버에는 영역 집합이 포함되어 있으며 각 영역은 사용자 관리를 위한 격리된 단위로 작동합니다. 이를 사전 구성하려면 JSON 형식으로 영역 정의 파일을 지정해야 합니다.

Keycloak 관리 콘솔을 사용하여 구성할 수 있는 모든 것은 이 JSON에 유지됩니다. 

인증 서버는 baeldung-realm.json 으로 사전 구성됩니다 . 파일에서 몇 가지 관련 구성을 살펴보겠습니다.

  • 사용자 : 기본 사용자는 john@test.commike@other.com입니다 . 또한 여기에 자격 증명이 있습니다.
  • 클라이언트 : id가 newClient인 클라이언트를 정의합니다.
  • standardFlowEnabled : newClient에 대한 인증 코드 흐름을 활성화하려면 true로 설정합니다.
  • redirectUris : 인증 성공 후 서버가 리디렉션할 newClient 의 URL이 여기에 나열됩니다.
  • webOrigins : "+"설정하여 redirectUris로 나열된 모든 URL에 대한 CORS 지원을 허용합니다.

Keycloak 서버는 기본적으로 JWT 토큰을 발급하므로 별도의 구성이 필요하지 않습니다. 다음으로 Maven 구성을 살펴보겠습니다.

3. 메이븐 설정

Spring Boot 애플리케이션 내부에 Keycloak을 내장할 것이므로 별도로 다운로드할 필요가 없습니다.

대신 다음 의존성 집합을 설정합니다 .

<dependency>
    <groupId>org.springframework.boot</groupId>        
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
       

여기에서는 Spring Boot의 2.4.10 버전을 사용하고 있습니다. 의존성을 위해 spring-boot-starter-data-jpa 및 H2가 추가되었습니다. 다른 springframework.boot 의존성은 웹 지원을 위한 것입니다. 또한 Keycloak 인증 서버와 관리 콘솔을 웹 서비스로 실행할 수 있어야 하기 때문입니다.

또한 Keycloak 및 RESTEasy에 대한 몇 가지 의존성이 필요합니다 .

<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-jackson2-provider</artifactId>
    <version>3.12.1.Final</version>
</dependency>

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-dependencies-server-all</artifactId>
    <version>11.0.2</version>
    <type>pom</type>
</dependency> 

Maven 사이트에서 KeycloakRESTEasy 의 최신 버전을 확인하십시오 .

그리고 마지막 으로 Spring Boot에서 정의한 버전 대신 Keycloak에서 선언한 버전을 사용 하려면 <infinispan.version>  속성 을 재정의해야 합니다  .

<properties>
    <infinispan.version>10.1.8.Final</infinispan.version>
</properties>

4.  내장된 Keycloak 구성

이제 인증 서버에 대한 Spring 구성을 정의해 보겠습니다.

@Configuration
public class EmbeddedKeycloakConfig {

    @Bean
    ServletRegistrationBean keycloakJaxRsApplication(
      KeycloakServerProperties keycloakServerProperties, DataSource dataSource) throws Exception {
        
        mockJndiEnvironment(dataSource);
        EmbeddedKeycloakApplication.keycloakServerProperties = keycloakServerProperties;
        ServletRegistrationBean servlet = new ServletRegistrationBean<>(
          new HttpServlet30Dispatcher());
        servlet.addInitParameter("javax.ws.rs.Application", 
          EmbeddedKeycloakApplication.class.getName());
        servlet.addInitParameter(ResteasyContextParameters.RESTEASY_SERVLET_MAPPING_PREFIX,
          keycloakServerProperties.getContextPath());
        servlet.addInitParameter(ResteasyContextParameters.RESTEASY_USE_CONTAINER_FORM_PARAMS, 
          "true");
        servlet.addUrlMappings(keycloakServerProperties.getContextPath() + "/*");
        servlet.setLoadOnStartup(1);
        servlet.setAsyncSupported(true);
        return servlet;
    }

    @Bean
    FilterRegistrationBean keycloakSessionManagement(
      KeycloakServerProperties keycloakServerProperties) {
        FilterRegistrationBean filter = new FilterRegistrationBean<>();
	filter.setName("Keycloak Session Management");
	filter.setFilter(new EmbeddedKeycloakRequestFilter());
	filter.addUrlPatterns(keycloakServerProperties.getContextPath() + "/*");

	return filter;
    }

    private void mockJndiEnvironment(DataSource dataSource) throws NamingException {		 
        NamingManager.setInitialContextFactoryBuilder(
          (env) -> (environment) -> new InitialContext() {
            @Override
            public Object lookup(Name name) {
                return lookup(name.toString());
            }
	
            @Override
            public Object lookup(String name) {
                if ("spring/datasource".equals(name)) {
                    return dataSource;
                }
                return null;
            }

            @Override
            public NameParser getNameParser(String name) {
                return CompositeName::new;
            }

            @Override
            public void close() {
            }
        });
    }
}

참고: 컴파일 오류에 대해 걱정하지 마십시오 . 나중에 EmbeddedKeycloakRequestFilter 클래스를 정의합니다  .

여기에서 볼 수 있듯이 먼저 영역 정의 파일에 지정된 Keycloak 속성의 영구 저장을 위해 KeycloakServerProperties가 있는 JAX-RS 애플리케이션으로 Keycloak구성했습니다 . 그런 다음 세션 관리 필터를 추가하고 메모리 내 H2 데이터베이스인 spring/datasource 를 사용하도록 JNDI 환경을 조롱했습니다 .

5. KeycloakServer속성

이제 방금 언급한 KeycloakServerProperties를 살펴보겠습니다 .

@ConfigurationProperties(prefix = "keycloak.server")
public class KeycloakServerProperties {
    String contextPath = "/auth";
    String realmImportFile = "baeldung-realm.json";
    AdminUser adminUser = new AdminUser();

    // getters and setters

    public static class AdminUser {
        String username = "admin";
        String password = "admin";

        // getters and setters        
    }
}

보시다시피 이것은 contextPath , adminUser 및 영역 정의 파일 을 설정하는 간단한 POJO 입니다.

6. EmbeddedKeycloakApplication

다음으로 이전에 설정한 구성을 사용하여 영역을 생성하는 클래스를 살펴보겠습니다.

public class EmbeddedKeycloakApplication extends KeycloakApplication {
    private static final Logger LOG = LoggerFactory.getLogger(EmbeddedKeycloakApplication.class);
    static KeycloakServerProperties keycloakServerProperties;

    protected void loadConfig() {
        JsonConfigProviderFactory factory = new RegularJsonConfigProviderFactory();
        Config.init(factory.create()
          .orElseThrow(() -> new NoSuchElementException("No value present")));
    }
    public EmbeddedKeycloakApplication() {
        createMasterRealmAdminUser();
        createBaeldungRealm();
    }

    private void createMasterRealmAdminUser() {
        KeycloakSession session = getSessionFactory().create();
        ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session);
        AdminUser admin = keycloakServerProperties.getAdminUser();
        try {
            session.getTransactionManager().begin();
            applianceBootstrap.createMasterRealmUser(admin.getUsername(), admin.getPassword());
            session.getTransactionManager().commit();
        } catch (Exception ex) {
            LOG.warn("Couldn't create keycloak master admin user: {}", ex.getMessage());
            session.getTransactionManager().rollback();
        }
        session.close();
    }

    private void createBaeldungRealm() {
        KeycloakSession session = getSessionFactory().create();
        try {
            session.getTransactionManager().begin();
            RealmManager manager = new RealmManager(session);
            Resource lessonRealmImportFile = new ClassPathResource(
              keycloakServerProperties.getRealmImportFile());
            manager.importRealm(JsonSerialization.readValue(lessonRealmImportFile.getInputStream(),
              RealmRepresentation.class));
            session.getTransactionManager().commit();
        } catch (Exception ex) {
            LOG.warn("Failed to import Realm json file: {}", ex.getMessage());
            session.getTransactionManager().rollback();
        }
        session.close();
    }
}

7. 맞춤형 플랫폼 구현

앞서 말했듯이 Keycloak은 RedHat/JBoss에서 개발했습니다. 따라서 응용 프로그램을 Wildfly 서버에 배포하거나 Quarkus 솔루션으로 배포하기 위한 기능 및 확장 라이브러리를 제공합니다.

In this case, we're moving away from those alternatives, and as a consequence, we have to provide custom implementations for some platform-specific interfaces and classes.

For example, in the EmbeddedKeycloakApplication we just configured we first loaded Keycloak's server configuration keycloak-server.json, using an empty subclass of the abstract JsonConfigProviderFactory:

public class RegularJsonConfigProviderFactory extends JsonConfigProviderFactory { }

Then, we extended KeycloakApplication to create two realms: master and baeldung. These are created as per the properties specified in our realm definition file, baeldung-realm.json.

As you can see, we use a KeycloakSession to perform all the transactions, and for this to work properly, we had to create a custom AbstractRequestFilter (EmbeddedKeycloakRequestFilter) and set up a bean for this using a KeycloakSessionServletFilter in the EmbeddedKeycloakConfig file.

Additionally, we need a couple of custom providers so that we have our own implementations of org.keycloak.common.util.ResteasyProvider and org.keycloak.platform.PlatformProvider and do not rely on external dependencies.

Importantly, information about these custom providers should be included in the project's META-INF/services folder so that they are picked up at runtime.

8. Bringing It All Together

As we saw, Keycloak has much simplified the required configurations from the application side. There is no need to programmatically define the datasource or any security configurations.

To bring it all together, we need to define the configuration for Spring and a Spring Boot Application.

8.1. application.yml

We'll be using a simple YAML for the Spring configurations:

server:
  port: 8083

spring:
  datasource:
    username: sa
    url: jdbc:h2:mem:testdb

keycloak:
  server:
    contextPath: /auth
    adminUser:
      username: bael-admin
      password: ********
    realmImportFile: baeldung-realm.json

8.2. Spring Boot Application

Lastly, here's the Spring Boot Application:

@SpringBootApplication(exclude = LiquibaseAutoConfiguration.class)
@EnableConfigurationProperties(KeycloakServerProperties.class)
public class AuthorizationServerApp {
    private static final Logger LOG = LoggerFactory.getLogger(AuthorizationServerApp.class);
    
    public static void main(String[] args) throws Exception {
        SpringApplication.run(AuthorizationServerApp.class, args);
    }

    @Bean
    ApplicationListener<ApplicationReadyEvent> onApplicationReadyEventListener(
      ServerProperties serverProperties, KeycloakServerProperties keycloakServerProperties) {
        return (evt) -> {
            Integer port = serverProperties.getPort();
            String keycloakContextPath = keycloakServerProperties.getContextPath();
            LOG.info("Embedded Keycloak started: http://localhost:{}{} to use keycloak", 
              port, keycloakContextPath);
        };
    }
}

Notably, here we have enabled the KeycloakServerProperties configuration to inject it into the ApplicationListener bean.

After running this class, we can access the authorization server's welcome page at http://localhost:8083/auth/.

9. Conclusion

In this quick tutorial, we saw how to setup a Keycloak server embedded in a Spring Boot application. The source code for this application is available over on GitHub.

The original idea for this implementation was developed by Thomas Darimont and can be found in the project embedded-spring-boot-keycloak-server.

Security footer banner