SpringBoot集成Keycloak角色权限配置问题咨询
问题1:修复拥有user角色的用户无法访问customers页面的问题
原因分析
Spring Security的hasRole("user")会自动校验前缀为ROLE_的权限,但Keycloak返回的角色默认存储在JWT的realm_access.roles字段中,且无ROLE_前缀;同时当前配置未将Keycloak的角色正确映射为Spring Security的GrantedAuthority,导致系统无法识别用户角色。
解决方案
- 添加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; }
- 在SecurityConfig中配置该转换器,替换默认JWT处理逻辑:
修改filterChain方法中的oauth2ResourceServer部分:
http.oauth2ResourceServer((oauth2) -> oauth2 .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())) );
- 适配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角色用户登录后的权限提示与跳转
解决方案
- 自定义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"); } }
- 在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) );
- 在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>
- 添加清除错误信息的接口(在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
相关产品推荐
相关产品推荐

