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

SpringBoot集成Keycloak角色权限配置问题咨询

问题1:修复拥有user角色的用户无法访问customers页面的问题

原因分析

Spring Security的hasRole("user")会自动校验前缀为ROLE_的权限,但Keycloak返回的角色默认存储在JWT的realm_access.roles字段中,且无ROLE_前缀;同时当前配置未将Keycloak的角色正确映射为Spring Security的GrantedAuthority,导致系统无法识别用户角色。

解决方案

  1. 添加JWT角色转换器,将Keycloak的realm角色转换为Spring Security认可的权限:
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    // 开启从realm_access.roles中提取角色
    grantedAuthoritiesConverter.setAuthoritiesClaimName("realm_access");
    grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); // 添加ROLE_前缀适配hasRole规则

    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
    return converter;
}
  1. 在SecurityConfig中配置该转换器,替换默认JWT处理逻辑:
    修改filterChain方法中的oauth2ResourceServer部分:
http.oauth2ResourceServer((oauth2) -> oauth2
        .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
);
  1. 适配OAuth2登录的角色映射(针对授权码模式下的OidcUser):
    在oauth2Login配置中添加userAuthoritiesMapper,将Keycloak返回的角色转换为Spring Security权限:
http.oauth2Login(oauth2 -> oauth2
        .defaultSuccessUrl("/customers", true)
        .failureUrl("/external")
        .userInfoEndpoint(userInfo -> userInfo
                .userAuthoritiesMapper(oidcUser -> {
                    List<GrantedAuthority> authorities = new ArrayList<>();
                    // 提取Keycloak中的realm角色
                    List<String> realmRoles = oidcUser.getClaim("realm_access").get("roles", List.class);
                    realmRoles.forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
                    return authorities;
                })
        )
)

问题2:无user角色用户登录后的权限提示与跳转

解决方案

  1. 自定义AccessDeniedHandler,处理权限不足的场景:
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 将权限提示存入session
        request.getSession().setAttribute("errorMessage", "您无所需访问权限");
        // 重定向到external页面
        response.sendRedirect("/external");
    }
}
  1. 在SecurityConfig中注入并配置该处理器:
    首先更新SecurityConfig的构造方法注入依赖:
private final KeycloakLogoutHandler keycloakLogoutHandler;
private final CustomAccessDeniedHandler customAccessDeniedHandler;

SecurityConfig(KeycloakLogoutHandler keycloakLogoutHandler, CustomAccessDeniedHandler customAccessDeniedHandler) {
    this.keycloakLogoutHandler = keycloakLogoutHandler;
    this.customAccessDeniedHandler = customAccessDeniedHandler;
}

然后在filterChain方法中添加异常处理配置:

http.exceptionHandling(exception -> exception
        .accessDeniedHandler(customAccessDeniedHandler)
);
  1. 在external页面显示提示信息(以Thymeleaf模板为例):
    在external.html中添加提示渲染逻辑:
<div th:if="${session.errorMessage}" class="alert alert-danger">
    <span th:text="${session.errorMessage}"></span>
    <script>
        // 3秒后清除session中的提示,避免刷新重复显示
        setTimeout(() => {
            fetch('/clear-error')
        }, 3000);
    </script>
</div>
  1. 添加清除错误信息的接口(在WebController中):
@GetMapping("/clear-error")
@ResponseBody
public void clearError(HttpServletRequest request) {
    request.getSession().removeAttribute("errorMessage");
}

最终完整SecurityConfig示例

@Configuration
@EnableWebSecurity
class SecurityConfig {

    private final KeycloakLogoutHandler keycloakLogoutHandler;
    private final CustomAccessDeniedHandler customAccessDeniedHandler;

    SecurityConfig(KeycloakLogoutHandler keycloakLogoutHandler, CustomAccessDeniedHandler customAccessDeniedHandler) {
        this.keycloakLogoutHandler = keycloakLogoutHandler;
        this.customAccessDeniedHandler = customAccessDeniedHandler;
    }

    @Bean
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        grantedAuthoritiesConverter.setAuthoritiesClaimName("realm_access");
        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        return converter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .requestMatchers("/customers*")
                .hasRole("user")
                .anyRequest()
                .permitAll();

        http.oauth2Login(oauth2 -> oauth2
                .defaultSuccessUrl("/customers", true)
                .failureUrl("/external")
                .userInfoEndpoint(userInfo -> userInfo
                        .userAuthoritiesMapper(oidcUser -> {
                            List<GrantedAuthority> authorities = new ArrayList<>();
                            List<String> realmRoles = oidcUser.getClaim("realm_access").get("roles", List.class);
                            realmRoles.forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
                            return authorities;
                        })
                )
        );

        http.logout(logout -> logout
                .logoutUrl("/logout")
                .addLogoutHandler(keycloakLogoutHandler)
                .logoutSuccessUrl("/"));

        http.oauth2ResourceServer((oauth2) -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
        );

        http.exceptionHandling(exception -> exception
                .accessDeniedHandler(customAccessDeniedHandler)
        );

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .build();
    }
}

内容的提问来源于stack exchange,提问作者Pau Perez

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 13:55:00