知識庫 / Spring / Spring Boot RSS 訂閱

Spring Boot 應用中嵌入 Keycloak

Security,Spring Boot
HongKong
4
01:02 PM · Dec 06 ,2025

1. 概述

Keycloak 是一個由 RedHat 維護和由 JBoss 基於 Java 開發的開源身份和訪問管理解決方案。

在本教程中,我們將學習如何在 Spring Boot 應用程序中嵌入 Keycloak 服務器。 這樣可以輕鬆啓動預配置的 Keycloak 服務器。

Keycloak 也可以作為獨立服務器運行,但需要下載並使用管理控制枱進行設置。

2. Keycloak 預配置

首先,讓我們瞭解如何預配置 Keycloak 服務器。

服務器包含一組 Realm,每個 Realm 作為一個隔離的單元進行用户管理。為了預配置它,我們需要指定一個 JSON 格式的 Realm 定義文件。

所有通過 Keycloak 管理控制枱 可配置的內容都保存在該 JSON 文件中。

我們的授權服務器將預配置使用 baeldung-realm.json。以下是該文件中一些相關的配置:

  • users: 我們的默認用户將是 [email protected][email protected]; 他們也將在此處擁有憑據
  • clients: 我們將定義一個客户端,其 ID 為 newClient
  • standardFlowEnabled: 設置為 true 以激活 newClient 的授權碼流程
  • redirectUris: newClient 的服務器將重定向到的 URL 列表,在成功身份驗證後,在此處列出
  • webOrigins: 設置為 “+” 以允許所有作為 redirectUris 列出的 URL 上的 CORS 支持

Keycloak 服務器默認會發出 JWT 令牌,因此不需要單獨進行配置。接下來,讓我們查看 Maven 配置。

3. Maven 配置由於我們將 Keycloak 嵌入到 Spring Boot 應用程序中,因此無需單獨下載。

相反,我們將設置以下依賴項:

<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-jpa 和 H2 依賴項。其他 springframework.boot 依賴項用於 Web 支持,因為我們還需要能夠以 Web 服務的方式運行 Keycloak 授權服務器和管理控制枱。

我們還需要以下依賴項用於 Keycloak 和 RESTEasy:

<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>24.0.4</version>
    <type>pom</type>
</dependency> 

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

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

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

請查閲 Maven 站點以獲取 Keycloak 和 RESTEasy 的最新版本:KeycloakRESTEasy

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("jakarta.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();
    }
}

注意:無需擔心編譯錯誤,稍後我們會定義 EmbeddedKeycloakRequestFilter 類。

如你所見,我們首先將 Keycloak 配置為 JAX-RS 應用程序,使用 KeycloakServerProperties 來持久存儲 Keycloak 屬性,正如我們在 realm 定義文件中所指定的那樣。然後我們添加了一個會話管理過濾器,並模擬了一個 JNDI 環境,以便使用 spring/datasource,即我們的內存中的 H2 數據庫。

5. KeycloakServerProperties

現在讓我們來查看一下剛才提到的 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        
    }
}

如我們所見,這是一個簡單的POJO,用於設置 contextPath, adminUser 和 realm 定義文件

6. 嵌入式Keycloak應用程序

接下來,讓我們查看該類,它使用我們在之前設置的配置來創建領域:

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. 自定義平台實現

正如我們所説,Keycloak 由 RedHat/JBoss 開發。因此,它提供了用於在 Wildfly 服務器上部署應用程序或作為 Quarkus 解決方案的功能和擴展庫。

在這種情況下,我們已經放棄了這些替代方案,因此必須為特定平台接口和類提供自定義實現。

例如,在我們剛剛配置的 EmbeddedKeycloakApplication 中,我們首先加載了 Keycloak 服務器的配置 keycloak-server.json,使用了抽象類 JsonConfigProviderFactory 的空子類:

public class RegularJsonConfigProviderFactory extends JsonConfigProviderFactory { }

然後,我們擴展了 KeycloakApplication,創建了兩個 Realm:masterbaeldung。 這些 Realm 是根據我們 Realm 定義文件 baeldung-realm.json 中指定的屬性創建的。

正如您所見,我們使用 KeycloakSession 執行所有事務,為了確保其正常工作,我們必須創建一個自定義的 AbstractRequestFilter (EmbeddedKeycloakRequestFilter) 並使用 KeycloakSessionServletFilterEmbeddedKeycloakConfig 文件中設置一個 Bean。

此外,我們需要一些自定義的 提供者,以便我們擁有自己的 org.keycloak.common.util.ResteasyProviderorg.keycloak.platform.PlatformProvider 實現,並且不依賴外部依賴項。

請注意,keycloack-server.json 文件中定義的的值只是佔位符。 除非鍵名以 “env.*” 前綴開頭,否則無法通過環境變量覆蓋它們。

要通過環境變量覆蓋變量,無論其前綴如何,都必須實現自定義 ProviderFactory 並覆蓋 getProperties 方法。

public class RegularJsonConfigProviderFactory extends JsonConfigProviderFactory {
    @Override
    protected Properties getProperties() {
        return new SystemEnvProperties(System.getenv());
    }
}

重要的是,關於這些自定義提供程序的詳細信息應包含在項目的 META-INF/services 文件夾中,以便在運行時被正確識別。

8. 整合所有內容

正如我們所見,Keycloak 極大地簡化了應用程序端所需配置。無需通過編程方式定義數據源或任何安全配置。

為了整合所有內容,我們需要定義 Spring 和 Spring Boot 應用程序的配置。

8.1. application.yml</h3

我們將使用簡單的 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 應用

最後,這裏是 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);
        };
    }
}

特別地,我們已啓用 KeycloakServerProperties 配置,將其注入到 ApplicationListener Bean 中。

運行此類後,我們可以通過 http://localhost:8083/auth/ 訪問授權服務器的歡迎頁面。

8.3. 可執行 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>

在此,我們指定了主類,並指示 Maven 解壓一些 Keycloak 依賴項。 這會在運行時從胖 JAR 包中解壓庫,並且現在我們可以使用標準 java -jar <artifact-name>命令運行應用程序。

授權服務器的歡迎頁面現在可訪問,如之前所示。

9. 結論

在本快速教程中,我們學習瞭如何在 Spring Boot 應用程序中嵌入 Keycloak 服務器的方法。

最初對該實現方案的設計由 Thomas Darimont 提出,可以在 embedded-spring-boot-keycloak-server 項目中找到。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.