Keycloak Embedded in a Spring Boot Application – 嵌入Spring Boot应用程序的Keycloak

最后修改: 2020年 3月 17日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Keycloak is an open-source Identity and Access Management solution administered by RedHat and developed in Java by JBoss.

Keycloak是一个开源的身份和访问管理解决方案,由RedHat管理,由JBoss用Java开发。

In this tutorial, we’ll learn how to set up a Keycloak server embedded in a Spring Boot application. This makes it easy to start up a pre-configured Keycloak server.

在本教程中,我们将学习如何设置嵌入Spring Boot应用程序中的Keycloak服务器。这使得启动一个预配置的Keycloak服务器变得很容易。

Keycloak can also be run as a standalone server, but then it involves downloading it and setup via the Admin Console.

Keycloak也可以作为独立服务器运行,但这需要下载它并通过管理控制台进行设置。

2. Keycloak Pre-Configuration

2.Keycloak的预配置

To start with, let’s understand how we can pre-configure a Keycloak server.

首先,让我们了解一下如何预先配置一个Keycloak服务器。

The server contains a set of realms, with each realm acting as an isolated unit for user management. To pre-configure it, we need to specify a realm definition file in a JSON format.

服务器包含一组境界,每个境界作为一个孤立的单元进行用户管理。为了预先配置,我们需要指定一个JSON格式的境界定义文件。

Everything that can be configured using the Keycloak Admin Console is persisted in this JSON.

所有可以使用Keycloak Admin Console进行配置的东西都被持久化在这个JSON中。

Our Authorization Server will be pre-configured with baeldung-realm.json. Let’s see a few relevant configurations in the file:

我们的授权服务器将预先配置好baeldung-realm.json。让我们看看文件中的一些相关配置。

  • users: our default users would be john@test.com and mike@other.com; they’ll also have their credentials here
  • clients: we’ll define a client with the id newClient
  • standardFlowEnabled: set to true to activate Authorization Code Flow for newClient
  • redirectUris: newClient‘s URLs that the server will redirect to after successful authentication are listed here
  • webOrigins: set to “+” to allow CORS support for all URLs listed as redirectUris

The Keycloak server issues JWT tokens by default, so there is no separate configuration required for that. Let’s look at the Maven configurations next.

Keycloak服务器默认发行JWT令牌,所以不需要为此单独配置。接下来我们来看看Maven的配置。

3. Maven Configuration

3.Maven配置

Since we’ll embed Keycloak inside of a Spring Boot application, there is no need to download it separately.

由于我们将把Keycloak嵌入到Spring Boot应用程序中,所以不需要单独下载它。

Instead, we’ll set up the following set of dependencies:

相反,我们将设置以下一组依赖关系

<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>
       

Note that we’re using Spring Boot’s 2.6.7 version here. The dependencies spring-boot-starter-data-jpa and H2 have been added for persistence. The other springframework.boot dependencies are for web support, as we also need to be able to run the Keycloak authorization server as well as admin console as web services.

注意,我们在这里使用的是Spring Boot的2.6.7版本。我们添加了spring-boot-starter-data-jpa和H2的依赖项来实现持久性。其他的springframework.boot依赖项是为了支持网络,因为我们还需要能够将Keycloak授权服务器以及管理控制台作为网络服务来运行。

We’ll also need a couple of dependencies for Keycloak and RESTEasy:

我们还需要Keycloak和RESTEasy的几个依赖项

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

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

Check the Maven site for the latest versions of Keycloak and RESTEasy.

检查Maven网站,查看KeycloakRESTEasy的最新版本。

And finally, we have to override the <infinispan.version> property, to use the version declared by Keycloak instead of the one defined by Spring Boot:

最后,我们必须覆盖<infinispan.version> 属性,以使用Keycloak声明的版本而不是Spring Boot定义的版本。

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

4. Embedded Keycloak Configuration

4.嵌入式密码锁配置

Now let’s define the Spring configuration for our authorization server:

现在让我们为我们的授权服务器定义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;
                } else if (name.startsWith("java:jboss/ee/concurrency/executor/")) {
                    return fixedThreadPool();
                }
                return null;
            }

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

            @Override
            public void close() {
            }
        });
    }
     
    @Bean("fixedThreadPool")
    public ExecutorService fixedThreadPool() {
        return Executors.newFixedThreadPool(5);
    }
     
    @Bean
    @ConditionalOnMissingBean(name = "springBootPlatform")
    protected SimplePlatformProvider springBootPlatform() {
        return (SimplePlatformProvider) Platform.getPlatform();
    }
}

Note: don’t worry about the compilation error, we’ll define the EmbeddedKeycloakRequestFilter class later on.

注意:不要担心编译错误,我们将在后面定义EmbeddedKeycloakRequestFilter类。

As we can see here, we first configured Keycloak as a JAX-RS application with KeycloakServerProperties for persistent storage of Keycloak properties as specified in our realm definition file. We then added a session management filter and mocked a JNDI environment to use a spring/datasource, which is our in-memory H2 database.

正如我们在这里所看到的,我们首先将Keycloak配置为一个JAX-RS应用程序,使用KeycloakServerProperties来持久化存储我们境界定义文件中指定的Keycloak属性。然后我们添加了一个会话管理过滤器,并模拟了一个JNDI环境来使用spring/datasource,也就是我们的内存H2数据库。

5. KeycloakServerProperties

5.KeycloakServerProperties

Now let’s have a look at the KeycloakServerProperties we just mentioned:

现在让我们看一下我们刚才提到的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        
    }
}

As we can see, this is a simple POJO to set the contextPath, adminUser and realm definition file.

我们可以看到,这是一个简单的POJO,用于设置contextPathadminUser和境界定义文件

6. EmbeddedKeycloakApplication

6.EmbeddedKeycloakApplication

Next, let’s see the class, which uses the configurations we set before, to create realms:

接下来,让我们看看这个类,它使用我们之前设定的配置,来创建境界。

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")));
    }
     
    @Override
    protected ExportImportManager bootstrap() {
        final ExportImportManager exportImportManager = super.bootstrap();
        createMasterRealmAdminUser();
        createBaeldungRealm();
        return exportImportManager;
    }

    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. Custom Platform Implementations

7.定制平台的实施

As we said, Keycloak is developed by RedHat/JBoss. Therefore, it provides functionality and extension libraries to deploy the application on a Wildfly server, or as a Quarkus solution.

正如我们所说,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:

例如,在我们刚刚配置的EmbeddedKeycloakApplication中,我们首先加载了Keycloak的服务器配置keycloak-server.json,使用抽象的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.

然后,我们扩展了KeycloakApplication,以创建两个领域。masterbaeldung。这些是按照我们的境界定义文件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.

正如你所看到的,我们使用一个KeycloakSession来执行所有的交易,为了使其正常工作,我们必须创建一个自定义的AbstractRequestFilter(EmbeddedKeycloakRequestFilter),并在EmbeddedKeycloakConfig文件中使用KeycloakSessionServletFilter为其设置一个bean。

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.

此外,我们需要几个自定义提供者,以便我们有自己的org.keycloak.common.util.ResteasyProviderorg.keycloak.platform.PlatformProvider的实现,不依赖外部的依赖性。

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.

重要的是,关于这些自定义提供者的信息应该包含在项目的META-INF/services文件夹中,以便它们在运行时被选中。

8. Bringing It All Together

8.把所有的东西集中起来

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.

正如我们所看到的,Keycloak已经大大简化了应用程序方面所需的配置。没有必要以编程方式定义数据源或任何安全配置。

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

为了将这一切结合起来,我们需要为Spring和Spring Boot应用程序定义配置。

8.1. application.yml

8.1.application.yml

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

我们将使用一个简单的YAML来实现Spring的配置。

server:
  port: 8083

spring:
  datasource:
    username: sa
    url: jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE

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

8.2. Spring Boot Application

8.2.Spring Boot应用程序

Lastly, here’s the Spring Boot Application:

最后,这里是Spring Boot应用程序。

@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.

值得注意的是,这里我们启用了KeycloakServerProperties配置,将其注入到ApplicationListenerBean中。

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

运行该类后,我们可以访问授权服务器的欢迎页面http://localhost:8083/auth/

8.3. Executable JAR

8.3.可执行的JAR

We can also create an executable jar file to package and run the application:

我们还可以创建一个可执行的jar文件来打包和运行应用程序。

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <mainClass>com.baeldung.auth.AuthorizationServerApp</mainClass>
        <requiresUnpack>
            <dependency>
                <groupId>org.keycloak</groupId>
                <artifactId>keycloak-connections-jpa</artifactId>
            </dependency>
            <dependency>
                <groupId>org.keycloak</groupId>
                <artifactId>keycloak-model-jpa</artifactId>
            </dependency>
        </requiresUnpack>
    </configuration>
</plugin>

Here, we’ve specified the main class and also instructed Maven to unpack some of the Keycloak dependencies. This unpacks the libraries from the fat jars at runtime and now we can run the application using the standard java -jar <artifact-name> command.

在这里,我们指定了主类,还指示Maven解压一些Keycloak的依赖。这在运行时将库从fat jars中解包出来,现在我们可以使用标准的java -jar <artifact-name>命令来运行该应用程序。

The authorizations server’s welcome page is now accessible, as shown previously.

现在可以访问授权服务器的欢迎页面了,如前所示。

9. Conclusion

9.结语

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.

在这个快速教程中,我们看到了如何设置一个嵌入Spring Boot应用程序的Keycloak服务器。该应用程序的源代码可在GitHub上获得

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

这个实现的原始想法是由Thomas Darimont开发的,可以在项目embedded-spring-boot-keycloak-server中找到。