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

Spring Boot 3集成Keycloak资源授权:hasPermission验证403问题解决

解决Keycloak资源授权中hasPermission注解403问题

针对Spring Boot 3 + Java 17 + Keycloak 21.1.1环境下,@PreAuthorize("hasPermission('my_ressource', 'add')")返回403的问题,需要修改Security配置,从JWT的authorization.permissions字段提取权限信息,具体步骤如下:

1. 自定义JWT权限转换器

实现JwtGrantedAuthoritiesConverter,同时保留原有角色解析逻辑,从RPT令牌的权限节点提取资源和操作:

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.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

public class KeycloakPermissionConverter implements Converter<Jwt, Collection<GrantedAuthority>> {

    @Override
    public Collection<GrantedAuthority> convert(Jwt jwt) {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        
        // 解析authorization.permissions中的资源权限
        Map<String, Object> authorization = jwt.getClaim("authorization");
        if (authorization != null && authorization.containsKey("permissions")) {
            List<Map<String, String>> permissions = (List<Map<String, String>>) authorization.get("permissions");
            for (Map<String, String> perm : permissions) {
                String resource = perm.get("resource");
                String action = perm.get("action");
                if (resource != null && action != null) {
                    authorities.add(new SimpleGrantedAuthority("PERMISSION_" + resource + ":" + action));
                }
            }
        }
        
        // 保留原realm角色解析,兼容已有的hasRole逻辑
        Map<String, Object> realmAccess = jwt.getClaim("realm_access");
        if (realmAccess != null && realmAccess.containsKey("roles")) {
            List<String> roles = (List<String>) realmAccess.get("roles");
            roles.forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
        }
        
        return authorities;
    }
}

2. 更新Security配置

在资源服务器配置中替换默认的JWT转换器,并开启方法级安全:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
            );
        return http.build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(new KeycloakPermissionConverter());
        return converter;
    }
}

3. 适配hasPermission注解使用

方案一:直接使用hasAuthority

如果不想额外配置,可以直接用格式化后的权限字符串:

@PreAuthorize("hasAuthority('PERMISSION_my_ressource:add')")

方案二:自定义PermissionEvaluator(保留原有注解写法)

如果想继续使用hasPermission('my_ressource', 'add'),需要自定义权限评估器:

import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.io.Serializable;

@Component
public class KeycloakPermissionEvaluator implements PermissionEvaluator {

    @Override
    public boolean hasPermission(Authentication auth, Object resource, Object action) {
        String requiredAuth = "PERMISSION_" + resource + ":" + action;
        return auth.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .anyMatch(requiredAuth::equals);
    }

    @Override
    public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object action) {
        return hasPermission(auth, targetType, action);
    }
}

然后配置方法级安全表达式处理器:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;

@Configuration
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    private final KeycloakPermissionEvaluator permissionEvaluator;

    public MethodSecurityConfig(KeycloakPermissionEvaluator permissionEvaluator) {
        this.permissionEvaluator = permissionEvaluator;
    }

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
        handler.setPermissionEvaluator(permissionEvaluator);
        return handler;
    }
}

完成以上配置后,@PreAuthorize("hasPermission('my_ressource', 'add')")即可正常生效。

关键注意点

  • 确认Keycloak颁发的RPT令牌中存在authorization.permissions字段,且每个权限条目包含resource和action属性
  • 转换器中保留了原角色解析逻辑,不会影响已有的hasRole功能
  • 权限前缀PERMISSION_可根据需求调整,只需保证转换器和评估器中的前缀一致即可

内容的提问来源于stack exchange,提问作者nihel turki

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 08:57:17