51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

在 Spring Boot 中嵌入 Keycloak 服务器

1、概览 {#1概览}

Keycloak 是由 Red Hat 管理和在 Java 中由 JBoss 开发的开源身份和访问管理解决方案。

本文将带你了解如何在 在 Spring Boot 中嵌入 Keycloak 服务器,这样就能轻松启动预配置的 Keycloak 服务器。

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

2、Keycloak 预配置 {#2keycloak-预配置}

服务器包含一组 Realm,每个 Realm 都是用户管理的独立单元。要对其进行预配置,我们需要指定一个 JSON 格式的 Realm 定义文件。

使用 Keycloak Admin 控制台 配置的所有内容都以 JSON 格式进行持久化。

我们的授权服务器将使用名为 baeldung-realm.json 的 JSON 文件进行预配置。文件中的几个相关配置如下:

  • users:默认用户是 john@test.commike@other.com;对应的凭证也在这里。
  • clients:定义一个 ID 为 newClient 的客户端
  • standardFlowEnabled:设置为 true,激活 newClient 的授权码(Authorization Code)授权模式。
  • redirectUrisnewClient 在成功验证后将重定向到的服务器 URL
  • webOrigins:置为 +,为所有 redirectUris 的 URL 提供 CORS 支持

Keycloak 服务器会默认签发 JWT Token,因此无需为此进行单独配置。接下来看看 Maven 的配置。

3、Maven 配置 {#3maven-配置}

由于在 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-validation</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 3.1.1 版本。添加了 spring-boot-starter-data-jpaH2 持久层依赖。其他 springframework.boot 依赖用于 Web 支持,因为还需要将 Keycloak 授权服务器和管理控制台作为 Web 服务运行。

还要为 KeycloakRESTEasy 添加以下依赖:

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

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

<dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-crypto-default</artifactId> <version>22.0.0</version> </dependency>

<dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-admin-ui</artifactId> <version>22.0.0</version> </dependency>

<dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-services</artifactId> <version>22.0.0</version> </dependency>

<dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-rest-admin-ui-ext</artifactId> <version>22.0.0</version> </dependency>

你可以从 Maven 仓库获取最新版本的 KeycloakRESTEasy

4、嵌入式 Keycloak 配置 {#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&lt;&gt;(
      new HttpServlet30Dispatcher());
    servlet.addInitParameter(&quot;jakarta.ws.rs.Application&quot;, 
      EmbeddedKeycloakApplication.class.getName());
    servlet.addInitParameter(ResteasyContextParameters.RESTEASY_SERVLET_MAPPING_PREFIX,
      keycloakServerProperties.getContextPath());
    servlet.addInitParameter(ResteasyContextParameters.RESTEASY_USE_CONTAINER_FORM_PARAMS, 
      &quot;true&quot;);
    servlet.addUrlMappings(keycloakServerProperties.getContextPath() + &quot;/*&quot;);
    servlet.setLoadOnStartup(1);
    servlet.setAsyncSupported(true);
    return servlet;
}

@Bean
FilterRegistrationBean keycloakSessionManagement(
  KeycloakServerProperties keycloakServerProperties) {
    FilterRegistrationBean filter = new FilterRegistrationBean&lt;&gt;();

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) -&gt; (environment) -&gt; new InitialContext() {
        @Override
        public Object lookup(Name name) {
            return lookup(name.toString());
        }

        @Override
        public Object lookup(String name) {
            if (&quot;spring/datasource&quot;.equals(name)) {
                return dataSource;
            } else if (name.startsWith(&quot;java:jboss/ee/concurrency/executor/&quot;)) {
                return fixedThreadPool();
            }
            return null;
        }

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

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

}

注意:先不用管编译错误,稍后会定义 EmbeddedKeycloakRequestFilter 类。

如上的,首先将 Keycloak 配置为 JAX-RS 应用,并使用 KeycloakServerProperties 来持久存储 Realm 定义文件中指定的 Keycloak 属性。然后,添加了一个会话管理过滤器(Session Management Filter),并模拟了一个 JNDI 环境,以使用 spring/datasource 也就是我们的存 H2 内数据库。

5、KeycloakServerProperties {#5keycloakserverproperties}

现在来看看上节提到的 KeycloakServerProperties

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

public static class AdminUser {
    String username = &quot;admin&quot;;
    String password = &quot;admin&quot;;

    // get、set  省略
}

}

如你所见,这是一个简单的 POJO,用于设置 contextPathadminUserrealm 定义文件。

6、EmbeddedKeycloakApplication {#6embeddedkeycloakapplication}

接着,来看看如下配置类,它使用之前设置的配置来创建 Realm。

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(() -&gt; new NoSuchElementException(&quot;No value present&quot;)));
}
 
@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(&quot;Couldn't create keycloak master admin user: {}&quot;, 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(&quot;Failed to import Realm json file: {}&quot;, ex.getMessage());
        session.getTransactionManager().rollback();
    }
    session.close();
}

}

7、自定义平台实现 {#7自定义平台实现}

如前所述,Keycloak 由 RedHat/JBoss 开发。因此,它提供了在 Wildfly 服务器上部署应用或作为 Quarkus 解决方案的功能和扩展库。

在本例中,我们放弃这些替代方案,因此,我们必须为一些特定平台的接口和类提供自定义实现。

例如,在刚刚配置的 EmbeddedKeycloakApplication 中,首先加载了 Keycloak 的服务器配置 keycloak-server.json,使用了一个空实现的 JsonConfigProviderFactory(抽象类)子类:

public class RegularJsonConfigProviderFactory extends JsonConfigProviderFactory { }

然后,继承了 KeycloakApplication,创建了两个 Realm:masterbaeldung。它们是根据 Realm 定义文件 baeldung-realm.json 中指定的属性创建的。

如你所见,使用 KeycloakSession 来执行所有事务,为使其正常工作,必须创建一个自定义 AbstractRequestFilterEmbeddedKeycloakRequestFilter),并在 EmbeddedKeycloakConfig 中使用 KeycloakSessionServletFilter 为其设置一个 Bean。

此外,还需要几个自定义 Provider,这样我们就能拥有自己的 org.keycloak.common.util.ResteasyProviderorg.keycloak.platform.PlatformProvider 实现,而无需依赖外部依赖。

注意,这些自定义 Provider 的信息需要包含在项目的 META-INF/services 文件夹中,以便在运行时获取。

8、把一切整合起来 {#8把一切整合起来}

Keycloak 大大简化了应用端所需的配置。无需以编程方式定义数据源或任何 Security 配置。

我们需要通过 Spring 和 Spring Boot 应用的配置,来把这一切整合起来。

8.1、application.yml {#81applicationyml}

使用 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 应用 {#82spring-boot-应用}

最后,是 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&lt;ApplicationReadyEvent&gt; onApplicationReadyEventListener(
  ServerProperties serverProperties, KeycloakServerProperties keycloakServerProperties) {
    return (evt) -&gt; {
        Integer port = serverProperties.getPort();
        String keycloakContextPath = keycloakServerProperties.getContextPath();
        LOG.info(&quot;Embedded Keycloak started: http://localhost:{}{} to use keycloak&quot;, 
          port, keycloakContextPath);
    };
}

}

注意,这里启用了 KeycloakServerProperties 配置,以便将其注入 ApplicationListener Bean。

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

8.3、可执行 JAR {#83可执行-jar}

还可以创建一个可执行 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-model-jpa</artifactId>
            </dependency>
        </requiresUnpack>
    </configuration>
</plugin>

配置如上,指定了 main 类,还指示 Maven 解压缩一些 Keycloak 依赖。这将在运行时解压 Fat Jar 中的库,现在可以使用标准的 java -jar <artifact-name> 命令运行应用了。

9、总结 {#9总结}

本文介绍了如何在 Spring Boot 中嵌入 Keycloak 服务器,此实现的最初想法由 Thomas Darimont 提出,可在 embedded-spring-boot-keycloak-server 项目中找到。


Ref:https://www.baeldung.com/keycloak-embedded-in-spring-boot-app

赞(6)
未经允许不得转载:工具盒子 » 在 Spring Boot 中嵌入 Keycloak 服务器