提问人:Samuel 提问时间:11/25/2022 最后编辑:Dmitriy PopovSamuel 更新时间:8/22/2023 访问量:28941
将Keycloak弹簧适配器与Spring Boot 3一起使用
Use Keycloak Spring Adapter with Spring Boot 3
问:
我在使用 Keycloak Spring Adapter 的项目中更新到 Spring Boot 3。不幸的是,它没有启动,因为它首先在 Spring Security 中被弃用,然后被删除。目前有没有另一种方法可以用Keycloak实现安全性?或者换句话说:如何将Spring Boot 3与Keycloak适配器结合使用?KeycloakWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter
我在互联网上搜索了一下,但找不到适配器的任何其他版本。
答:
使用标准的 Spring Security OAuth2 客户端而不是特定的 Keycloak 适配器,而不是 .SecurityFilterChain
WebSecurityAdapter
像这样的东西:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true)
class OAuth2SecurityConfig {
@Bean
fun customOauth2FilterChain(http: HttpSecurity): SecurityFilterChain {
log.info("Configure HttpSecurity with OAuth2")
http {
oauth2ResourceServer {
jwt { jwtAuthenticationConverter = CustomBearerJwtAuthenticationConverter() }
}
oauth2Login {}
csrf { disable() }
authorizeRequests {
// Kubernetes
authorize("/readiness", permitAll)
authorize("/liveness", permitAll)
authorize("/actuator/health/**", permitAll)
// ...
// everything else needs at least a valid login, roles are checked at method level
authorize(anyRequest, authenticated)
}
}
return http.build()
}
然后在:application.yml
spring:
security:
oauth2:
client:
provider:
abc:
issuer-uri: https://keycloak.../auth/realms/foo
registration:
abc:
client-secret: ...
provider: abc
client-id: foo
scope: [ openid, profile, email ]
resourceserver:
jwt:
issuer-uri: https://keycloak.../auth/realms/foo
评论
@PreAuthorise
authoriseRequests
由于您发现的原因,您不能将 Keycloak 适配器与 spring-boot 3 一起使用,以及其他一些与传递依赖项相关的适配器。由于大多数 Keycloak 适配器在 2022 年初被弃用,因此很可能不会发布更新来解决这个问题。
相反,请将 spring-security 6 库用于 OAuth2。不要惊慌,使用 spring-boot 这是一项简单的任务。
在下文中,我将认为您对 OAuth2 概念有很好的理解,并且确切地知道为什么需要配置 OAuth2 客户端或 OAuth2 资源服务器。如有疑问,请参阅我的教程的 OAuth2 要点部分。
我在这里只详细介绍servlet应用程序作为资源服务器的配置,然后作为客户端,对于单个Keycloak领域,有和没有,我的Spring Boot启动器。直接浏览到你感兴趣的部分(但如果你不想使用“我的”启动器,请准备好编写更多的代码)。spring-addons-starter-oidc
另请参阅我的教程,了解不同的用例,例如:
- 接受由多个领域或实例(预先已知或在受信任域中动态创建)颁发的令牌
- 反应式应用程序(Webflux),例如
spring-cloud-gateway
- 公开提供 REST API 和服务器端呈现的 UI 以使用它的应用
- 高级访问控制规则
- BFF模式
- ...
1. OAuth2 资源服务器
应用公开了受访问令牌保护的 REST API。它由 OAuth2 REST 客户端使用。此类客户端的几个示例:
- 另一个配置为 OAuth2 客户端并使用 、 或类似方式的 Spring 应用程序
WebClient
@FeignClient
RestTemplate
- 像 Postman 这样的开发工具,能够获取 OAuth2 令牌并发出 REST 请求
- 基于 Javascript 的应用程序配置为“公共”OAuth2 客户端,带有类似 angular-auth-oidc-client 的库
1.1. 使用spring-addons-starter-oidc
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-oidc</artifactId>
<version>7.1.3</version>
</dependency>
origins: http://localhost:4200
issuer: http://localhost:8442/realms/master
com:
c4-soft:
springaddons:
oidc:
ops:
- iss: ${issuer}
username-claim: preferred_username
authorities:
- path: $.realm_access.roles
prefix: ROLE_
- path: $.resource_access.*.roles
resourceserver:
cors:
- path: /**
allowed-origin-patterns: ${origins}
permit-all:
- "/actuator/health/readiness"
- "/actuator/health/liveness"
- "/v3/api-docs/**"
上面的 conf 中领域角色的前缀仅用于说明目的,您可以将其删除。CORS 配置也需要一些改进。
@Configuration
@EnableMethodSecurity
public static class WebSecurityConfig { }
无需再为资源服务器配置经过微调的 CORS 策略和颁发机构映射。战利品,不是吗?
从属性是一个数组中可以猜到,这个解决方案实际上与“静态”多租户兼容:你可以根据需要声明任意数量的受信任颁发者,它可以是异构的(对用户名和权限使用不同的声明)。ops
此外,该解决方案与响应式应用程序兼容:将从类路径上的内容中检测它并调整其安全自动配置。spring-addons-starter-oidc
1.2. 只需spring-boot-starter-oauth2-resource-server
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<!-- used when converting Keycloak roles to Spring authorities -->
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8442/realms/master
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public static class WebSecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http, Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter) throws Exception {
http.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter)));
// Enable and configure CORS
http.cors(cors -> cors.configurationSource(corsConfigurationSource("http://localhost:4200")));
// State-less session (state in access-token only)
http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// Disable CSRF because of state-less session-management
http.csrf(csrf -> csrf.disable());
// Return 401 (unauthorized) instead of 302 (redirect to login) when
// authorization is missing or invalid
http.exceptionHandling(eh -> eh.authenticationEntryPoint((request, response, authException) -> {
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Bearer realm=\"Restricted Content\"");
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
}));
// @formatter:off
http.authorizeHttpRequests(accessManagement -> accessManagement
.requestMatchers("/actuator/health/readiness", "/actuator/health/liveness", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
);
// @formatter:on
return http.build();
}
private UrlBasedCorsConfigurationSource corsConfigurationSource(String... origins) {
final var configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(origins));
configuration.setAllowedMethods(List.of("*"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setExposedHeaders(List.of("*"));
final var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@RequiredArgsConstructor
static class JwtGrantedAuthoritiesConverter implements Converter<Jwt, Collection<? extends GrantedAuthority>> {
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public Collection<? extends GrantedAuthority> convert(Jwt jwt) {
return Stream.of("$.realm_access.roles", "$.resource_access.*.roles").flatMap(claimPaths -> {
Object claim;
try {
claim = JsonPath.read(jwt.getClaims(), claimPaths);
} catch (PathNotFoundException e) {
claim = null;
}
if (claim == null) {
return Stream.empty();
}
if (claim instanceof String claimStr) {
return Stream.of(claimStr.split(","));
}
if (claim instanceof String[] claimArr) {
return Stream.of(claimArr);
}
if (Collection.class.isAssignableFrom(claim.getClass())) {
final var iter = ((Collection) claim).iterator();
if (!iter.hasNext()) {
return Stream.empty();
}
final var firstItem = iter.next();
if (firstItem instanceof String) {
return (Stream<String>) ((Collection) claim).stream();
}
if (Collection.class.isAssignableFrom(firstItem.getClass())) {
return (Stream<String>) ((Collection) claim).stream().flatMap(colItem -> ((Collection) colItem).stream()).map(String.class::cast);
}
}
return Stream.empty();
})
/* Insert some transformation here if you want to add a prefix like "ROLE_" or force upper-case authorities */
.map(SimpleGrantedAuthority::new)
.map(GrantedAuthority.class::cast).toList();
}
}
@Component
@RequiredArgsConstructor
static class SpringAddonsJwtAuthenticationConverter implements Converter<Jwt, JwtAuthenticationToken> {
@Override
public JwtAuthenticationToken convert(Jwt jwt) {
final var authorities = new JwtGrantedAuthoritiesConverter().convert(jwt);
final String username = JsonPath.read(jwt.getClaims(), "preferred_username");
return new JwtAuthenticationToken(jwt, authorities, username);
}
}
}
除了比前一个解决方案更冗长之外,此解决方案还不太灵活:
- 不适用于多租户(多个Keycloak领域或实例)
- 硬编码允许的源
- 硬编码的声明名称以从中获取自动性
- 硬编码的“permitAll”路径匹配器
2. OAuth2 客户端
应用公开使用会话(而不是访问令牌)保护的任何类型的资源。它由浏览器(或任何其他能够维护会话的用户代理)直接使用,不需要脚本语言或 OAuth2 客户端库(授权代码流、注销和令牌存储由服务器上的 Spring 处理)。常见用例包括:
- 具有服务器端呈现 UI 的应用程序(使用 Thymeleaf、JSF 或其他任何方法)
spring-cloud-gateway
用作 Backend F 或 Frontend:配置为带有过滤器的 OAuth2 客户端(在将请求转发到下游资源服务器之前,从浏览器中隐藏 OAuth2 令牌,并用访问令牌替换会话 cookie)TokenRelay
请注意,Back-Channel Logout
尚未由 Spring 实现。如果需要,请使用“我的”启动器(或从中复制)。
2.1. 使用spring-addons-starter-oidc
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-client</artifactId>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-oidc</artifactId>
<version>7.1.3</version>
</dependency>
issuer: http://localhost:8442/realms/master
client-id: spring-addons-confidential
client-secret: change-me
client-uri: http://localhost:8080
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: ${issuer}
registration:
keycloak-login:
authorization-grant-type: authorization_code
client-name: My Keycloak instance
client-id: ${client-id}
client-secret: ${client-secret}
provider: keycloak
scope: openid,profile,email,offline_access
com:
c4-soft:
springaddons:
oidc:
ops:
- iss: ${issuer}
username-claim: preferred_username
authorities:
- path: $.realm_access.roles
- path: $.resource_access.*.roles
client:
client-uri: ${client-uri}
security-matchers: /**
permit-all:
- /
- /login/**
- /oauth2/**
csrf: cookie-accessible-from-js
post-login-redirect-path: /home
post-logout-redirect-path: /
back-channel-logout-enabled: true
@Configuration
@EnableMethodSecurity
public class WebSecurityConfig {
}
至于资源服务器,此解决方案也适用于反应式应用程序。
客户端上还有一个可选的多租户支持:允许用户同时登录多个OpenID提供程序,他可能有不同的用户名(默认情况下,这是Keycloak中的UUID,并随每个领域而变化)。subject
2.2. 只需spring-boot-starter-oauth2-client
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<!-- used when converting Keycloak roles to Spring authorities -->
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>
issuer: http://localhost:8442/realms/master
client-id: spring-addons-confidential
client-secret: change-me
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: ${issuer}
registration:
keycloak-login:
authorization-grant-type: authorization_code
client-name: My Keycloak instance
client-id: ${client-id}
client-secret: ${client-secret}
provider: keycloak
scope: openid,profile,email,offline_access
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class WebSecurityConfig {
@Bean
SecurityFilterChain
clientSecurityFilterChain(HttpSecurity http, InMemoryClientRegistrationRepository clientRegistrationRepository)
throws Exception {
http.oauth2Login(withDefaults());
http.logout(logout -> {
logout.logoutSuccessHandler(new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository));
});
// @formatter:off
http.authorizeHttpRequests(ex -> ex
.requestMatchers("/", "/login/**", "/oauth2/**").permitAll()
.requestMatchers("/nice.html").hasAuthority("NICE")
.anyRequest().authenticated());
// @formatter:on
return http.build();
}
@Component
@RequiredArgsConstructor
static class GrantedAuthoritiesMapperImpl implements GrantedAuthoritiesMapper {
@Override
public Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (OidcUserAuthority.class.isInstance(authority)) {
final var oidcUserAuthority = (OidcUserAuthority) authority;
final var issuer = oidcUserAuthority.getIdToken().getClaimAsURL(JwtClaimNames.ISS);
mappedAuthorities.addAll(extractAuthorities(oidcUserAuthority.getIdToken().getClaims()));
} else if (OAuth2UserAuthority.class.isInstance(authority)) {
try {
final var oauth2UserAuthority = (OAuth2UserAuthority) authority;
final var userAttributes = oauth2UserAuthority.getAttributes();
final var issuer = new URL(userAttributes.get(JwtClaimNames.ISS).toString());
mappedAuthorities.addAll(extractAuthorities(userAttributes));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
});
return mappedAuthorities;
};
@SuppressWarnings({ "rawtypes", "unchecked" })
private static Collection<GrantedAuthority> extractAuthorities(Map<String, Object> claims) {
/* See resource server solution above for authorities mapping */
}
}
}
3. 什么是spring-addons-starter-oidc
,为什么要使用它
此启动器是标准的 Spring Boot 启动器,具有其他应用程序属性,用于自动配置默认 Bean 并将其提供给 Spring Security。需要注意的是,自动配置的@Beans
几乎都是@ConditionalOnMissingBean
这使您能够在 conf 中覆盖它。
它是开源的,您可以更改它为您预先配置的所有内容(请参阅 Javadoc、入门 README 或许多示例)。在决定不信任它之前,您应该阅读启动器源代码,它并没有那么大。从 imports
资源开始,它定义了 Spring Boot 为自动配置加载的内容。
在我看来(如上所述),OAuth2 的 Spring Boot 自动配置可以进一步推动:
- 使 OAuth2 配置更具可移植性:使用可配置的权限转换器,只需编辑属性(Keycloak、Auth0、Cognito、Azure AD 等),即可从 OIDC 提供程序切换到另一个提供程序。
- 简化不同环境中的应用部署:CORS 配置由属性文件控制
- 大幅减少 Java 代码量(如果您在多租户场景中,事情会变得更加复杂)
- 默认支持多个颁发者
- 减少错误配置的可能性(例如,经常看到在客户端上禁用 CSRF 保护的示例配置,或者在使用访问令牌保护的端点上浪费资源)
评论
spring.security.oauth2.client.registration.keycloak.client-id=smartorganizr spring.security.oauth2.client.registration.keycloak.client-secret=dzmKY0QUuLflQBeceMIhPCr8gE5AN9YF spring.security.oauth2.client.provider.keycloak.issuer-uri=http://192.168.2.33/realms/master
无法使用 Keycloak 适配器,因为继承自该类,该类在 Spring Security 中已弃用,随后在较新版本中删除。KeycloakWebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter
我在 Medium 上发表了一篇关于将 Keycloak 与 Spring Boot 3.0 集成的详细文章,其中提供了有关如何将 Keycloak 与 Spring Boot 3.0 集成的分步指南。
本指南对于那些不熟悉将Keycloak与Spring Boot 3.0集成或从旧版本迁移到Spring Boot 3.0的用户特别有用。
您可以查看文章 (https://medium.com/geekculture/using-keycloak-with-spring-boot-3-0-376fa9f60e0b) 以全面解释集成过程。
希望这有帮助!如果您有任何问题、进一步的澄清或建议,请随时发表评论。
评论
Keycloak 21.0.0引入了一些新的变化来支持Spring Security 6.x.x和Spring Boot 3.x.x.。这是对它的引用
评论
Keycloak适配器已弃用,Keycloak团队宣布的未来不会有任何更新或修复。
建议使用 Spring Security 提供的 OAuth2 和 OpenID Connect 支持。
根据不同的资源和整个周末,我花了来解决这个新问题,我设法找到了完美的解决方案。
我定义了 2 个角色:客户端级别(而不是领域)的用户和管理员,并分配给不同的用户。
- JDK 17的
- 钥匙斗篷 22.0.0。
- Spring Boot 3.1.1 (英语)
以下是以下工作解决方案:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class OAuth2ResourceServerSecurityConfiguration {
@Value("${keycloak.resource}")
private String keycloakClientName;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests((authorize) -> {
authorize
.anyRequest().authenticated();
})
.oauth2ResourceServer(httpSecurityOAuth2ResourceServerConfigurer -> httpSecurityOAuth2ResourceServerConfigurer
.jwt(jwtConfigurer -> {
jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter());
})
);
return httpSecurity.build();
}
private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter());
return jwtAuthenticationConverter;
}
private class KeycloakRealmRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
private Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
final Map<String, Object> resourceAccess = (Map<String, Object>) jwt.getClaims().get("resource_access");
if (resourceAccess != null) {
final Map<String, Object> clientAccess = (Map<String, Object>) resourceAccess.get(OAuth2ResourceServerSecurityConfiguration.this.keycloakClientName);
if (clientAccess != null) {
grantedAuthorities = ((List<String>) clientAccess.get("roles")).stream()
.map(roleName -> "ROLE_" + roleName) // Prefix to map to a Spring Security "role"
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
return grantedAuthorities;
}
}
}
属性中的Keycloak配置:
keycloak:
authServerUrl: http://<your_keycloak_host>:8989
realm: <your_realm>
resource: <your_client>
useResourceRoleMappings: true
cors: true
corsMaxAge: 1000
corsAllowedMethods: POST, PUT, DELETE, GET
sslRequired: none
bearerOnly: true
publicClient: true
principalAttribute: preferred_username
credentials:
secret: '{cipher}<your_encrypted_secret>'
和测试控制器:
@RestController
@RequestMapping("/api/v1/test")
public class TestController {
@GetMapping("/")
public String allAccess() {
return "Public content";
}
@GetMapping("/endpoint1")
@PreAuthorize("hasRole('user')")
public String endpoint1() {
return "User board";
}
@GetMapping("/endpoint2")
@PreAuthorize("hasRole('administrator')")
public String endpoint2() {
return "Administrator board";
}
}
评论
keycloak
keycloak.resource
spring.security.oauth2.resourceserver
评论