如何在Spring Boot WebFlux中集成Keycloak Policy Enforcer?
Spring Boot WebFlux集成Keycloak授权策略方案
目前Keycloak官方没有为Spring WebFlux提供原生的Policy Enforcer支持,ServletPolicyEnforcerFilter依赖Servlet API,无法在响应式环境中运行。以下是两种可行的替代方案,覆盖角色级和细粒度资源级授权场景:
方案一:基于Keycloak角色的响应式RBAC授权
这种方案利用Spring Security的Reactive OAuth2资源服务器模块,解析Keycloak颁发的JWT令牌,提取角色信息并完成授权校验,适合简单的角色控制场景。
1. 添加依赖
在pom.xml中引入必要依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security-webflux</artifactId> </dependency>
2. 配置Keycloak JWT验证
在application.yml中配置Keycloak Realm的issuer地址:
spring: security: oauth2: resourceserver: jwt: issuer-uri: http://your-keycloak-server/auth/realms/your-realm-name
3. 编写安全配置类
通过ServerHttpSecurity定义授权规则,并自定义JWT转换器提取Keycloak角色:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.server.SecurityWebFilterChain; @Configuration @EnableWebFluxSecurity public class SecurityConfig { @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { http .authorizeExchange(exchanges -> exchanges .pathMatchers("/public/**").permitAll() .pathMatchers("/admin/**").hasRole("ADMIN") // 对应Keycloak中定义的角色,Spring自动添加ROLE_前缀 .pathMatchers("/user/**").hasAnyRole("USER", "ADMIN") .anyExchange().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(new KeycloakJwtRoleConverter()))); return http.build(); } }
4. JWT角色转换器
将Keycloak JWT中的realm_access.roles字段转换为Spring Security的权限对象:
import org.springframework.core.convert.converter.Converter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; public class KeycloakJwtRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> { @Override public Collection<GrantedAuthority> convert(Jwt jwt) { List<String> realmRoles = jwt.getClaimAsStringList("realm_access.roles"); return realmRoles.stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) .collect(Collectors.toList()); } }
方案二:基于Keycloak资源的细粒度授权
如果需要复用Keycloak中定义的资源、权限和策略(对应Policy Enforcer的功能),可以通过调用Keycloak Authorization API实现响应式的细粒度授权。
1. 添加Keycloak Admin Client依赖
<dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-admin-client</artifactId> <version>22.0.5</version> <!-- 需与你的Keycloak版本一致 --> </dependency>
2. 编写授权检查服务
封装Keycloak Authorization API调用,转为响应式方法:
import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.KeycloakBuilder; import org.keycloak.representations.idm.authorization.AuthorizationRequest; import org.keycloak.representations.idm.authorization.AuthorizationResponse; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @Component public class KeycloakAuthorizationService { private final Keycloak keycloakClient; public KeycloakAuthorizationService() { // 生产环境建议从配置文件读取参数 this.keycloakClient = KeycloakBuilder.builder() .serverUrl("http://your-keycloak-server/auth") .realm("your-realm-name") .clientId("your-client-id") .clientSecret("your-client-secret") .grantType(OAuth2Constants.CLIENT_CREDENTIALS) .build(); } /** * 校验用户是否拥有指定资源的权限 * @param accessToken 用户的JWT令牌 * @param resourceKey Keycloak中定义的资源Key * @param permissionKey Keycloak中定义的权限Key * @return 是否授权通过 */ public Mono<Boolean> checkPermission(String accessToken, String resourceKey, String permissionKey) { AuthorizationRequest request = new AuthorizationRequest(); request.addPermission(resourceKey, permissionKey); return Mono.fromCallable(() -> { AuthorizationResponse response = keycloakClient.realm("your-realm-name") .authorization() .authorize(accessToken, request); return response.getPermissions() != null && !response.getPermissions().isEmpty(); }).subscribeOn(Schedulers.boundedElastic()); // 将阻塞API调用转为响应式 } }
3. 自定义授权过滤器
在WebFlux过滤器中调用授权服务,完成细粒度校验:
import org.springframework.http.HttpStatus; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; @Component public class KeycloakResourceAuthorizationFilter implements WebFilter { private final KeycloakAuthorizationService authorizationService; public KeycloakResourceAuthorizationFilter(KeycloakAuthorizationService authorizationService) { this.authorizationService = authorizationService; } @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { // 根据请求路径映射到Keycloak的资源和权限,可自定义映射逻辑 String requestPath = exchange.getRequest().getPath().value(); String resourceKey = requestPath.startsWith("/api/orders") ? "order-resource" : "default-resource"; String permissionKey = exchange.getRequest().getMethod().name().equalsIgnoreCase("GET") ? "view" : "edit"; return ReactiveSecurityContextHolder.getContext() .map(ctx -> ctx.getAuthentication()) .filter(auth -> auth.getPrincipal() instanceof Jwt) .map(auth -> (Jwt) auth.getPrincipal()) .flatMap(jwt -> authorizationService.checkPermission(jwt.getTokenValue(), resourceKey, permissionKey)) .flatMap(isAuthorized -> { if (isAuthorized) { return chain.filter(exchange); } else { exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); return exchange.getResponse().setComplete(); } }) .switchIfEmpty(Mono.defer(() -> { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); })); } }
4. 注册过滤器到安全配置
修改之前的SecurityConfig,添加自定义过滤器:
@Configuration @EnableWebFluxSecurity public class SecurityConfig { private final KeycloakResourceAuthorizationFilter authorizationFilter; public SecurityConfig(KeycloakResourceAuthorizationFilter authorizationFilter) { this.authorizationFilter = authorizationFilter; } @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { http .authorizeExchange(exchanges -> exchanges .pathMatchers("/public/**").permitAll() .anyExchange().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(new KeycloakJwtRoleConverter()))) .addFilterAfter(authorizationFilter, org.springframework.security.web.server.authentication.AuthenticationWebFilter.class); return http.build(); } }
注意事项
- 方案一性能更优,无需额外调用Keycloak API,适合简单RBAC场景;方案二完全复用Keycloak的授权策略,但需注意API调用的性能,建议添加缓存优化
- 确保Keycloak版本与依赖版本一致,避免兼容性问题
- 生产环境中,Keycloak客户端配置(clientId、clientSecret等)应通过环境变量或配置中心管理,禁止硬编码
内容的提问来源于stack exchange,提问作者Minh Trần
相关产品推荐
相关产品推荐

