将 Spring Security 全局方法安全性与 Keycloak 集成

Posted

技术标签:

【中文标题】将 Spring Security 全局方法安全性与 Keycloak 集成【英文标题】:Integrating Spring Security Global Method Security with Keycloak 【发布时间】:2019-09-13 18:55:57 【问题描述】:

我在使用 Spring Security 中的 Pre/Post Authorization Annotations 和带有 Keycloak 集成的 Servlet API 时遇到问题。我调查了很多文章、教程和以下问题没有进一步的运气

Obtaining user roles in servlet application using keycloak Spring Boot Keycloak - How to get a list of roles assigned to a user? Using spring security annotations with keycloak Spring Boot + Spring Security + Hierarchical Roles How do I add method based security to a Spring Boot project? Configure DefaultMethodSecurityExpressionHandler using Spring Security Java Config SpringBoot + method based hierarchical roles security: ServletContext is required

我想要的只是删除 ROLES_ 前缀,使用分层角色和一种舒适的方式来检索用户的角色。

到目前为止,我可以在 Controller 中检索这样的分层角色,但不能使用注释:

@Controller
class HomeController 

    @Autowired
    AccessToken token

    @GetMapping('/')
    def home(Authentication auth, HttpServletRequest request) 
        // Role 'admin' is defined in Keycloak for this application
        assert token.getResourceAccess('my-app').roles == ['admin']
        // All effective roles are mapped
        assert auth.authorities.collect  it.authority .containsAll(['admin', 'author', 'user'])

        // (!) But this won't work:
        assert request.isUserInRole('admin')
    

    // (!) Leads to a 403: Forbidden
    @GetMapping('/sec')
    @PreAuthorize("hasRole('admin')") 
        return "Hello World"
    


我猜@PreAuthorize 注释不起作用,因为那个 Servlet 方法不成功。

在 Keycloak 和 Spring 中只定义了三个角色——管理员、作者、用户:

enum Role 
    USER('user'),
    AUTHOR('author'),
    ADMIN('admin')

    final String id

    Role(String id) 
        this.id = id
    

    @Override
    String toString() 
        id
    

密钥斗篷配置

从该网络安全中删除 @EnableGlobalMethodSecurity 注释后,会显示由 No ServletContext set 错误引起的 Error creating bean with name 'resourceHandlerMapping' - 不知道它来自哪里!

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter 

    /**
     * Registers the KeycloakAuthenticationProvider with the authentication manager.
     */
    @Autowired
    void configureGlobal(AuthenticationManagerBuilder auth) 
        auth.authenticationProvider(keycloakAuthenticationProvider().tap  provider ->
            // Assigns the Roles via Keycloaks role mapping
            provider.grantedAuthoritiesMapper = userAuthoritiesMapper
        )
    

    @Bean
    RoleHierarchyImpl getRoleHierarchy() 
        new RoleHierarchyImpl().tap 
            hierarchy = "$Role.ADMIN > $Role.AUTHOR > $Role.USER"
        
    

    @Bean
    GrantedAuthoritiesMapper getUserAuthoritiesMapper() 
        new RoleHierarchyAuthoritiesMapper(roleHierarchy)
    

    SecurityExpressionHandler<FilterInvocation> expressionHandler() 
        // Removes the prefix
        new DefaultWebSecurityExpressionHandler().tap 
            roleHierarchy = roleHierarchy
            defaultRolePrefix = null
        
    

    // ...

    @Bean
    @Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
    AccessToken accessToken() 
        def request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()
        def authToken = (KeycloakAuthenticationToken) request.userPrincipal
        def securityContext = (KeycloakSecurityContext) authToken.credentials

        return securityContext.token
    

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        super.configure(http)
        http
            .authorizeRequests()
            .expressionHandler(expressionHandler())
            // ...
    


全局方法安全配置

我需要明确允许allow-bean-definition-overriding,否则我会收到bean with that name already defined 错误,这表明我完全失去了对整个情况的控制,不知道发生了什么。

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
class GlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration 

    @Autowired
    RoleHierarchy roleHierarchy

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() 
        ((DefaultMethodSecurityExpressionHandler)super.createExpressionHandler()).tap 
            roleHierarchy = roleHierarchy
            defaultRolePrefix = null
        
    

任何其他可能很重要的配置?非常感谢您的帮助!

【问题讨论】:

isUserInRole 将检查用户是否具有ROLE_admin 角色,但事实并非如此。在 Spring Security 中进行角色检查时,默认情况下会自动添加 ROLE_ 前缀。而不是使用hasRole 使用hasAuthority,它不会添加ROLE_ 前缀。 这就是为什么我将 defaultRolePrefix 设置为 null,这应该可以解决问题。 不是真的,因为您还需要修改其他一些地方。 Spring Security 参考指南中有一个示例,它修改了 4 个不同位置的前缀,使用 BeanPostProcessor 而不是重新声明所有 bean。 【参考方案1】:

正如M. Deinum 指出的那样,必须在多个位置用BeanPostProcessor 删除defaultRolePrefix,这在(docs.spring.io) Disable ROLE_ Prefixing 中有说明。

这种方法对我来说似乎不是很干净,所以我编写了一个自定义AuthoritiesMapper 来实现从 Keycloak 映射分层角色,而无需将它们重命名为 ROLE_ Spring 标准。首先,修改了Roles 枚举以符合应用程序范围内的标准:

enum Role 
    USER('ROLE_USER'),
    AUTHOR('ROLE_AUTHOR'),
    ADMIN('ROLE_ADMIN')

    // ...

其次,我用前缀分层实现替换了RoleHierarchyAuthoritiesMapper

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter 

    // ..

    // Replaces the RoleHierarchyAuthoritiesMapper
    @Bean
    GrantedAuthoritiesMapper getUserAuthoritiesMapper() 
        new PrefixingRoleHierarchyAuthoritiesMapper(roleHierarchy)
    


class PrefixingRoleHierarchyAuthoritiesMapper extends RoleHierarchyAuthoritiesMapper 

        String prefix = 'ROLE_'

        PrefixingRoleHierarchyAuthoritiesMapper(RoleHierarchy roleHierarchy) 
            super(roleHierarchy)
        

        @Override
        Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) 
            def prefixedAuthorities = authorities.collect  GrantedAuthority originalAuthority ->
                new GrantedAuthority() 
                    String authority = "$prefix$originalAuthority.authority".toUpperCase()
                
            

            super.mapAuthorities(prefixedAuthorities)
        
    

最后,我摆脱了GlobalMethodSecurityConfig

【讨论】:

【参考方案2】:

除了(docs.spring.io) Disable ROLE_ Prefixing提供的建议和M. Deinum提供的建议外,在使用KeycloakWebSecurityConfigurerAdapter时还需要做一点修改。

在 configureGlobal 方法中,grantedAuthoritiesMapper bean 被设置在 bean keycloakAuthenticationProvider 中。而在grantedAuthoritiesMapper中,prefix可以任意设置,默认值为“ROLE_”。

代码如下:

    @Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception 
    KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();

    SimpleAuthorityMapper grantedAuthoritiesMapper = new SimpleAuthorityMapper();
    grantedAuthoritiesMapper.setPrefix("");
    keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthoritiesMapper);
    auth.authenticationProvider(keycloakAuthenticationProvider);

这个解决方案对我有用。

【讨论】:

以上是关于将 Spring Security 全局方法安全性与 Keycloak 集成的主要内容,如果未能解决你的问题,请参考以下文章

Spring Security 3.2 - 配置全局方法安全性以使用角色层次结构

Spring Security:全局方法安全性不起作用

使用 Spring Security 3.2.4 for Jersey 2.9 REST 服务的全局方法安全性的正确配置是啥?

如何将 Spring Security 与我现有的 REST Web 服务集成

Spring Security

Spring Security 学习总结