在 Spring Boot 中嵌入 Keycloak 服务器
1、概览
Keycloak 是由 Red Hat 管理和在 Java 中由 JBoss 开发的开源身份和访问管理解决方案。
本文将带你了解如何在 在 Spring Boot 中嵌入 Keycloak 服务器,这样就能轻松启动预配置的 Keycloak 服务器。
Keycloak 也可以作为 独立服务器 运行,但需要下载并通过管理控制台进行设置。
2、Keycloak 预配置
服务器包含一组 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 配置
由于在 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 配置
为授权服务器定义 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
现在来看看上节提到的 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
接着,来看看如下配置类,它使用之前设置的配置来创建 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、自定义平台实现
如前所述,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、把一切整合起来
Keycloak 大大简化了应用端所需的配置。无需以编程方式定义数据源或任何 Security 配置。
我们需要通过 Spring 和 Spring Boot 应用的配置,来把这一切整合起来。
8.1、application.yml
使用 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>
配置如上,指定了 main
类,还指示 Maven 解压缩一些 Keycloak 依赖。这将在运行时解压 Fat Jar 中的库,现在可以使用标准的 java -jar <artifact-name>
命令运行应用了。
9、总结
本文介绍了如何在 Spring Boot 中嵌入 Keycloak 服务器,此实现的最初想法由 Thomas Darimont 提出,可在 embedded-spring-boot-keycloak-server 项目中找到。
Ref:https://www.baeldung.com/keycloak-embedded-in-spring-boot-app