Spring-Security-OAuth2 同时校验多种权限

最近有个新项目需要我搭建一下,需要对接第三方用户系统,对方使用的是keycloak认证中心平台,所以只需要拿到对方的/certs地址就可以进行对用户的请求头Authorizationtoken进行签名的校验,然后获取token中的权限和一些信息内容

1.pom.xml中引入spring-security依赖
<!--security start-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!--security end-->

因为目前只做鉴权用户和识别用户权限,只需要这两个就够了

2.配置application.yml中的授权地址
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: http://project.com/auth/realms/realms-name/protocol/openid-connect/certs

基本认证中心的地址都长的差不多

3.配置SecurityConfig
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.sessionManagement().sessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy())
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().authorizeRequests()
                //允许无权限访问
                .antMatchers("/api/v1/sample/messages").permitAll()             .antMatchers("/api/v1/sample/**").hasAnyAuthority("SCOPE_ligafi.end","ligafi.end")
                .anyRequest().authenticated()
                .and()
                .oauth2ResourceServer().authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                .jwt();
    }   
}
4.写一个SampleController用来测试可行性
@RestController
@RequestMapping("/api/v1/sample")
@Api(value = "A Sample Controller", tags = {"Demo Tag"})
public class SampleController extends BaseController{
    
 	@PostMapping("/messages")
    @ApiOperation("from user get message")
    public String message(){
        return "this is a test message";
    }   
    
    @GetMapping("/getParam")
    @ApiOperation("getParam")
    public String getParam() {
        return "this is a test getParam";
    }

}    

这里的Swagger引入和配置就不说了,需要注意的就是在SecurityConfig加一个允许的权限,不然Security会进行权限拦截

这样:

@Override
public void configure(WebSecurity web)  {
    web.ignoring().antMatchers("/v2/api-docs/**")
            .antMatchers("/swagger-ui.html")
            .antMatchers("/swagger-resources/**")
            .antMatchers("/webjars/**");
}
5.找到keycloak平台获取token的地址,获取一个token进行测试

https://project.com/auth/realms/realms-name/protocol/openid-connect/token

该平台因为需要校验用户的权限ROLE和客户端的权限SCOPE,这些JWT(Json Web Token)里面的内容就不做解释了,去https://jwt.io/就可以了解到了

从图中可以看到,获取到了token,尝试解析一下token

现在第三方调用我们业务的时候需要同时校验realm_access中的roles里面的ligafi.end权限和客户端的scope权限ligafi.end,现在去Swagger试试

image.png

需要注意的是,请求头的AuthorizationBearer类型的token

看着好像是通过了,但是我们通过在源码里面打断点看看情况

org.springframework.security.access.expression.SecurityExpressionRoot#hasAnyAuthorityName

获取到的权限里面只有客户端的scope部分,而且hasAnyAuthority("SCOPE_ligafi.end","ligafi.end")的校验中,只要有一项权限符合要求就通过,所以不能同时校验客户端scope和用户的roles

先处理无法获取用户权限的问题

在配置SecurityConfig中自定义一个AuthenticationConverter

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.sessionManagement().sessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy())
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().authorizeRequests()
                //允许无权限访问
                .antMatchers("/api/v1/sample/messages").permitAll()
                //先校验客户端权限,接口校验用户权限,因为hasAnyAuthority不能实现hasEveryAuthority,需要分开校验
                .antMatchers("/api/v1/sample/**").hasAuthority("SCOPE_ligafi.end")
                .anyRequest().authenticated()
                .and()
                .oauth2ResourceServer().authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                .jwt()
                .jwtAuthenticationConverter(grantedAuthoritiesExtractorConverter());
    }

    @Bean
    Converter<Jwt, AbstractAuthenticationToken> grantedAuthoritiesExtractorConverter() {
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesExtractor());
        return jwtAuthenticationConverter;
    }

    @Bean
    GrantedAuthoritiesExtractor grantedAuthoritiesExtractor() {
        return new GrantedAuthoritiesExtractor();
    }

    @Override
    public void configure(WebSecurity web)  {
        web.ignoring().antMatchers("/v2/api-docs/**")
                .antMatchers("/swagger-ui.html")
                .antMatchers("/swagger-resources/**")
                .antMatchers("/webjars/**");
    }

}

通过@EnableGlobalMethodSecurity(prePostEnabled = true)开启方法上的注解@PreAuthorize来校验权限

GrantedAuthoritiesExtractor

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

    @Override
    public Collection<GrantedAuthority> convert(Jwt jwt) {
        List<GrantedAuthority> authorities = new ArrayList<>();
        String realmAccess = "realm_access";
        String roles = "roles";
        String scope = "scope";
        if (jwt.containsClaim(realmAccess)) {
            JSONObject realmAccessJson = (JSONObject) jwt.getClaims().get(realmAccess);
            if (realmAccessJson.containsKey(roles)) {
                JSONArray realmRoles = (JSONArray) realmAccessJson.get(roles);
                for (Object realmRole : realmRoles) {
                    authorities.add(new SimpleGrantedAuthority("ROLE_" +realmRole.toString()));
                }
            }
        }
        if (jwt.containsClaim(scope)) {
            String scopeStr = (String) jwt.getClaims().get(scope);
            if (!StringUtils.isEmpty(scopeStr) && !scopeStr.isEmpty()) {
                String[] scopes = scopeStr.split("\\s");
                for (String scopeAuthority : scopes) {
                    authorities.add(new SimpleGrantedAuthority("SCOPE_" + scopeAuthority));
                }
            }
        }
        return authorities;
    }
}

ROLE_SCOPE_来区分客户端和用户的权限

在方法上添加注解

 @GetMapping("/getParam")
 @PreAuthorize("hasRole('ligafi.end')")
 @ApiOperation("getParam")
 public String getParam() {
     return "this is a test getParam";
 }

hasRole会自动帮权限加上ROLE_,所以我们之前就直接authorities.add(new SimpleGrantedAuthority("ROLE_" +realmRole.toString()))自己手动加上。

再去Swagger试试

现在所有权限都获取到了,而且校验了两次,所以这样校验是正确的方式

成功返回结果。

总结:最主要的地方就是SecurityConfig中自定义的jwtAuthenticationConverter 和方法上的注解@PreAuthorize("hasRole('ligafi.end')"),因为不能同时校验两个权限,目前想到的方式就是分开校验。