You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 09:44:51