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.com
和mike@other.com
;对应的凭证也在这里。clients
:定义一个 ID 为newClient
的客户端standardFlowEnabled
:设置为true
,激活newClient
的授权码(Authorization Code)授权模式。redirectUris
:newClient
在成功验证后将重定向到的服务器 URLwebOrigins
:置为+
,为所有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-jpa 和 H2 持久层依赖。其他 springframework.boot 依赖用于 Web 支持,因为还需要将 Keycloak 授权服务器和管理控制台作为 Web 服务运行。
还要为 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>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 仓库获取最新版本的 Keycloak 和 RESTEasy。
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<>( 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
来持久存储 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 = "admin"; String password = "admin"; // get、set 省略 }
}
如你所见,这是一个简单的 POJO,用于设置 contextPath
、adminUser
和 realm
定义文件。
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(() -> 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、自定义平台实现 {#7自定义平台实现}
如前所述,Keycloak 由 RedHat/JBoss 开发。因此,它提供了在 Wildfly 服务器上部署应用或作为 Quarkus 解决方案的功能和扩展库。
在本例中,我们放弃这些替代方案,因此,我们必须为一些特定平台的接口和类提供自定义实现。
例如,在刚刚配置的 EmbeddedKeycloakApplication
中,首先加载了 Keycloak 的服务器配置 keycloak-server.json
,使用了一个空实现的 JsonConfigProviderFactory
(抽象类)子类:
public class RegularJsonConfigProviderFactory extends JsonConfigProviderFactory { }
然后,继承了 KeycloakApplication
,创建了两个 Realm:master
和 baeldung
。它们是根据 Realm 定义文件 baeldung-realm.json
中指定的属性创建的。
如你所见,使用 KeycloakSession
来执行所有事务,为使其正常工作,必须创建一个自定义 AbstractRequestFilter
(EmbeddedKeycloakRequestFilter
),并在 EmbeddedKeycloakConfig
中使用 KeycloakSessionServletFilter
为其设置一个 Bean。
此外,还需要几个自定义 Provider,这样我们就能拥有自己的 org.keycloak.common.util.ResteasyProvider
和 org.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<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 {#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