Sam*_*uel 28 spring-boot keycloak
我在一个使用 Keycloak Spring Adapter 的项目中更新到了 Spring Boot 3。不幸的是,它没有启动,因为它 KeycloakWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter首先在 Spring Security 中被弃用,然后被删除。目前是否有另一种方法可以使用 Keycloak 实现安全性?或者换句话说:如何将 Spring Boot 3 与 Keycloak 适配器结合使用?
我在互联网上搜索,但找不到任何其他版本的适配器。
ch4*_*4mp 64
由于您发现的原因以及其他一些与传递依赖项相关的原因,您无法将 Keycloak 适配器与 spring-boot 3 一起使用。由于大多数 Keycloak 适配器已于 2022 年初被弃用,因此很可能不会发布任何更新来解决该问题。
相反,对 OAuth2 使用 spring-security 6 库。不要惊慌,使用 spring-boot 这很容易完成。
在下文中,我将认为您对 OAuth2 概念有很好的理解,并且确切地知道为什么需要配置 OAuth2 客户端或 OAuth2 资源服务器。如有疑问,请参阅我的教程的OAuth2 要点部分。
我在这里只会详细介绍servlet应用程序作为资源服务器的配置,然后作为客户端,对于单个 Keycloak 领域,使用和不使用spring-addons-starter-oidc我的 Spring Boot 启动器。直接浏览到您感兴趣的部分(但如果您不想使用“我的”启动器,请准备好编写更多代码)。
另请参阅我的教程以了解不同的用例,例如:
spring-cloud-gateway例如应用程序公开使用访问令牌保护的 REST API。它由 OAuth2 REST 客户端使用。此类客户的一些示例:
WebClient、@FeignClient或RestTemplate类似方法来查询资源服务器spring-cloud-gatewayoauth2Login()TokenRelayspring-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.5.3</version>
</dependency>
Run Code Online (Sandbox Code Playgroud)
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: /my-resources/**
allowed-origin-patterns: ${origins}
permit-all:
- "/actuator/health/readiness"
- "/actuator/health/liveness"
- "/v3/api-docs/**"
Run Code Online (Sandbox Code Playgroud)
上面conf中领域角色的前缀仅用于说明目的,您可以将其删除。CORS 配置也需要一些改进。
@Configuration
@EnableMethodSecurity
public static class WebSecurityConfig { }
Run Code Online (Sandbox Code Playgroud)
无需再配置具有微调的 CORS 策略和权限映射的资源服务器。很不错,不是吗?。
正如您可以从ops数组属性中猜到的那样,该解决方案实际上与“静态”多租户兼容:您可以根据需要声明任意多个受信任的颁发者,并且它可以是异构的(对用户名和权限使用不同的声明)。
此外,该解决方案与反应式应用程序兼容:spring-addons-starter-oidc将从类路径上的内容检测它并调整其安全自动配置。
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>
Run Code Online (Sandbox Code Playgroud)
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8442/realms/master
Run Code Online (Sandbox Code Playgroud)
@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("/my-resources/**", 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);
}
}
}
Run Code Online (Sandbox Code Playgroud)
除了比前一种解决方案更加冗长之外,该解决方案也不太灵活:
应用程序公开使用会话(不是访问令牌)保护的任何类型的资源。它由浏览器(或任何其他能够维护会话的用户代理)直接使用,无需脚本语言或 OAuth2 客户端库(授权代码流、注销和令牌存储由服务器上的 Spring 处理)。常见用例有:
spring-cloud-gateway用作后端F或前端:配置oauth2Login有过滤器(在将请求转发到下游资源服务器之前,从浏览器隐藏 OAuth2 令牌并用TokenRelay访问令牌替换会话 cookie)。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.5.3</version>
</dependency>
Run Code Online (Sandbox Code Playgroud)
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:
provider: keycloak
authorization-grant-type: authorization_code
client-id: ${client-id}
client-secret: ${client-secret}
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: /
Run Code Online (Sandbox Code Playgroud)
@Configuration
@EnableMethodSecurity
public class WebSecurityConfig {
}
Run Code Online (Sandbox Code Playgroud)
至于资源服务器,该解决方案也适用于反应式应用程序。
客户端上还有一个可选的多租户支持:允许用户同时登录多个 OpenID 提供商,他可能有不同的用户名(subject默认情况下,这是 Keycloak 中的 UUID,并且随每个领域而变化) 。
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>
Run Code Online (Sandbox Code Playgroud)
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:
provider: keycloak
authorization-grant-type: authorization_code
client-id: ${client-id}
client-secret: ${client-secret}
scope: openid,profile,email,offline_access
Run Code Online (Sandbox Code Playgroud)
@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 */
}
}
}
Run Code Online (Sandbox Code Playgroud)
spring-addons-starter-oidc以及为什么使用它该启动器是一个标准的Spring Boot 启动器,具有附加的应用程序属性,用于自动配置默认 bean 并将其提供给 Spring Security。需要注意的是,@Beans几乎所有自动配置都@ConditionalOnMissingBean使您能够在 conf 中覆盖它。
它是开源的,您可以更改它为您预先配置的所有内容(请参阅 Javadoc、入门自述文件或许多示例)。在决定不信任它之前,您应该阅读初学者的源代码,它并没有那么大。从importsresource开始,它定义了Spring Boot加载的内容以进行自动配置。
在我看来(正如上面所演示的),OAuth2 的 Spring Boot 自动配置可以进一步推进到:
使用标准 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()
}
Run Code Online (Sandbox Code Playgroud)
然后在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
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
36045 次 |
| 最近记录: |