[Spring Security] 整合Keycloak,默认只会获取Realm下的角色列表的解决方案

[Spring Security] 整合Keycloak,默认只会获取Realm下的角色列表的解决方案

Xy718 1,257 2022-07-20

这段时间公司要开发几个生产业务系统的管理模块,用Keycloak的LDAP做用户体系

使用OAuth2及jwt方式来完成用户认证,不过认证部分非常简单,只需要获取jwt就可以了,详见:

当完成登录后携带相关的jwt访问接口时你会发现,任何接口security都会返回403异常,这是因为用户虽然完成了认证,但是没有相关接口的权限,即便你使用http.authorizeRequests().anyRequest().hasAnyRole("xxx"); 指明了只需要某角色就可以访问某接口,还是没有用,这是因为Keycloak官方的默认Provider不具备使用应用角色上下文的功能。

安装配置Keycloak和引入SpringSecurity这里不再赘述,只放出关键代码

默认的Security Keycloak:

@KeycloakConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Import({KeycloakSpringBootConfigResolver.class})
public class KeycloakAdapterConfig extends KeycloakWebSecurityConfigurerAdapter {

...


    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

...

}

这一部分注册的是Keycloak官方的默认Provider,关键源码:

#org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider.java

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
    List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>();

    for (String role : token.getAccount().getRoles()) {
        grantedAuthorities.add(new KeycloakRole(role));
    }
    return new KeycloakAuthenticationToken(token.getAccount(), token.isInteractive(), mapAuthorities(grantedAuthorities));
}

阅读源码和断点可以发现,token.getAccount().getRoles()最终会使用登录后的jwt中的realm_access作为用户上下文的角色列表,如果在业务管理模块中需要细分resource应用的角色的话,这种方式是不完善的。

所以我们需要自定义一个Provider,来完成Keycloak用户认证后的上下文的应用角色注入:

import org.keycloak.adapters.springboot.KeycloakSpringBootProperties;
import org.keycloak.adapters.springsecurity.account.KeycloakRole;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class CustomKeycloakAuthenticationProvider extends KeycloakAuthenticationProvider {
    private GrantedAuthoritiesMapper grantedAuthoritiesMapper;

    KeycloakSpringBootProperties keycloakSpringBootProperties;

    public void setGrantedAuthoritiesMapper(GrantedAuthoritiesMapper grantedAuthoritiesMapper) {
        this.grantedAuthoritiesMapper = grantedAuthoritiesMapper;
    }
    public void setKeycloakSpringBootProperties(KeycloakSpringBootProperties keycloakSpringBootProperties){
        this.keycloakSpringBootProperties=keycloakSpringBootProperties;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
        List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>();

        for (String role : token.getAccount().getRoles()) {
            grantedAuthorities.add(new KeycloakRole(role));
        }
        //在下面注入指定应用的角色列表,当然也可以把上面的realm角色部分注释掉
        for (String role : token.getAccount().getKeycloakSecurityContext().getToken()
                .getResourceAccess(keycloakSpringBootProperties.getResource()).getRoles()) {
            grantedAuthorities.add(new KeycloakRole(role));
        }
        return new KeycloakAuthenticationToken(token.getAccount(), token.isInteractive(), mapAuthorities(grantedAuthorities));
    }

    private Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
        return grantedAuthoritiesMapper != null
                ? grantedAuthoritiesMapper.mapAuthorities(authorities)
                : authorities;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return KeycloakAuthenticationToken.class.isAssignableFrom(aClass);
    }
    
}

上面这段代码就是我们的自定义Provider了,其中新加了一个keycloakSpringBootProperties属性,是为了使用在配置文件中配置的keycloak.resource作为我们的需要注入角色列表的应用,这一块可以根据需要酌情修改

然后修改KeycloakAdapterConfig:

@KeycloakConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Import({KeycloakSpringBootConfigResolver.class})
public class KeycloakAdapterConfig extends KeycloakWebSecurityConfigurerAdapter {

...

    @Resource
    KeycloakSpringBootProperties keycloakSpringBootProperties;
    
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        CustomKeycloakAuthenticationProvider customKeycloakAuthenticationProvider=new CustomKeycloakAuthenticationProvider();
        customKeycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        customKeycloakAuthenticationProvider.setKeycloakSpringBootProperties(keycloakSpringBootProperties);
        auth.authenticationProvider(customKeycloakAuthenticationProvider);
    }

...

}

即可

这个时候当使用HttpSecurity或者@PreAuthorize("hasRole('xxx')")配置需要指定角色的接口时,就不会再出现403异常~

image-1658307061522


冶心·练体·得技