Spring极简资源服务器

254 阅读6分钟


-本文为填坑文-

  很早之前我在《Spring cloud gatway适配Keycloak》中给自己留了一个坑。\

可以看出token中已包含有用户相关信息,可进一步传递给下游,作为权限判断输入,符合上文中的预期。好了,用户登录有了,剩下的就是在资源服务器(即下游微服务)上实现权限验证了。

后面再单独写一章吧,因为资源服务器的实现略有不同。

  其实能鉴权的资源服务器很早就实现了,不过因为最近在愉快的玩儿其他有意思的东东,没能顾上填坑,正好最近天气不错,那就把它填了吧。

  先来看看效果。

  在用户未登录的情况下,点击任意链接(根据Gateway配置判断是否需要登录访问)都会跳转至Keycloak以完成SSO流程,这一点在之前的文章中已经有过介绍。

  但是对于微服务框架(功能尽可能解耦)来讲,Gateway最好只作为用户的访问入口(将访问跳转至服务提供者)或特殊时段的流量分流器(比如灰度版本上线时)。而用户是否有权限访问/使用某一具体服务,应该由对应的该服务提供者来判断,Gateway路由时将相关的信息(如token)完整传递下去就好,这里的服务提供者就是资源服务器。

服务权限限定

  • POM依赖配置
    既然资源服务器要对访问的用户进行鉴权,那么需要首先给暴露的端点加上权限限定,这在Spring框架里倒是挺简单,只需要在POM中加入spring-boot-starter-oauth2-resource即可。
<dependency>\
       <groupId>org.springframework.boot</groupId>\
       <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>\
</dependency>\

  它会自动引用Security相关依赖,因此不用再单独引入。

  • 权限配置
    有了resource-server依赖之后,就可以对端点进行权限(访问)定义了。

  resource-server默认使用WebSecurityConfigurerAdapter作为其安全配置器,其初始状态为:所有端点均需要登录后才能访问,换句话表述即为所有端点一视同仁。但是这并不是我们想要的效果(通常来讲,不同端点有不同的访问权限限制才是较常见的情况)。因此我们需要继承WebSecurityConfigurerAdapter,并重写里面的 protected void configure(HttpSecurity) 方法。

protected void configure(HttpSecurity http) throws Exception{\
        http.authorizeRequests(authz -> authz\
                .antMatchers(HttpMethod.GET, "/testing/").permitAll()\
                .antMatchers(HttpMethod.GET, "/testing/auth/**").hasRole(role_auth)\
                .antMatchers(HttpMethod.GET, "/testing/vip/**").hasRole(role_vip)\
                .antMatchers(HttpMethod.GET, "/testing/admin/**").hasRole(role_admin)\
                .anyRequest().authenticated())\
                .oauth2ResourceServer().jwt().jwtAuthenticationConverter(new JwtAuthenticationConverter());\
}\

  代码很简单,将所有需要控制的端点按各自访问需求配置即可。又因为KeyCloak返回的是jwt格式token,所以配完权限设定后,我们告诉资源服务器使用JWT进行token解析。

  按道理讲这样配置后就可以正常工作了,但是实际是无法完成校验的(所有访问均无法通过)。

Token解析\

  我们将KeyCloak返回的token解码后会发现一些有意思的事儿。因为我的鉴权是基于role的,因此将token里面与role相关的字段筛选出来,如下:

{\
  ……\
  "realm_access": {\
    "roles": [\
      "offline_access",\
      "admin",\
      ……\
    ]\
  },\
  "resource_access": {\
    "realm-management": {\
      "roles": [\
        "view-realm",\
        "view-identity-providers",\
        "manage-identity-providers",\
        "impersonation",\
        "realm-admin",\
        "create-client",\
         ……\
      ]\
    },\
    "account": {\
      "roles": [\
        "manage-account",\
        "manage-account-links",\
        "view-profile"\
      ]\
    }\
  },\
  "scope": "profile email",\
  ……\
  "roles": [\
    "ROLE_offline_access",\
    "ROLE_vip",\
    "ROLE_user",\
    ……\
  ],\
  ……\
}\

  可以看出token中有好几个与role相关的字段,彼此之间还都有差别,我们依次解读一下。

  1. realm_access.roles 用户具有的外部自定义权限,即提供给资源服务器鉴权用的。\
  2. resource_access 用户具有的内部权限,主要是指访问KeyCloak子功能的权限。
    - realm-management.roles 管理当前realm的权限(如:创建、删除、查看等)
    - account.roles 对当前账户的管理权限(如:查看、更改等)\
  3. roles 所有已分配的角色描述

备注:2与3 是可以一一对应上的

  token中角色信息基本上就这些了,有用的的都有了,不怎么有用的也在token当中。回头再看看Spring Security是怎么取的这些信息呢?

  经过一系列的定位,跟读代码,我发现Spring读取的信息是token中的scope字段(别问怎么跟的,也别问具体是那个文件,因为过去太久了,我也忘记了),而scope字段的值是profile email。所以默认情况下永远也无法校验通过(当然也可以把角色名改成profile email →_→)。

Token适配\

  怎么解决这个问题呢?至少有两种办法值得尝试。

  - 修改KeyCloak产生的Token,使其适配Spring解析规则。

- 修改Spring解析方式,适应KeyCloak。
\

KeyCloak映射\

  KeyCloak提供了丰富而强大的映射功能,在上文解析token内容时所发现的ROLE_信息即通过映射的方式将自定义的角色信息映射到token自定义字段当中的。

  然而就算有如此强大丰富的映射功能,我们也不能将realm.roles信息映射到scope中,并不是无法配置这样的映射规则,而是配置之后在校验时会有很多莫名其妙的问题。

如果有好心人知道如何正确配置,也请不吝赐教

自定义解析器\

  之前我们跟踪Spring Security会发现在使用JWT解析器的时候,实际的转换是通过JwtGrantedAuthoritiesConverter 中的covert完成。解析方法到也不复杂,简单来说就是读取jwt token的scpe字段。要解决这个问题就得用我们自己covert去替换默认的。

  幸运的是Spring Security开放了相关接口。只需要用自定义的解析方法替换即可。

protected void configure(HttpSecurity http) throws Exception{\
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();\
        converter.setJwtGrantedAuthoritiesConverter(new Converter<Jwt, Collection<GrantedAuthority>>() {\
            @Override\
            public Collection<GrantedAuthority> convert(Jwt source) {\
                JSONArray roles = (JSONArray) ((JSONObject) (source.getClaims().get("realm_access"))).get("roles");\
//                JSONArray scopes = (JSONArray) ((JSONObject) (source.getClaims().get("resource_access"))).get("roles");\
                ArrayList<GrantedAuthority> roleArray = new ArrayList<>();\
                for(int i = 0; i < roles.size(); i++){\
                    roleArray.add(new Role(roles.get(i)));\
                }\
                return roleArray;\
            }\
        });\
        http.authorizeRequests(authz -> authz\
                .antMatchers(HttpMethod.GET, "/testing/").permitAll()\
                .antMatchers(HttpMethod.GET, "/testing/auth/**").hasRole(role_auth)\
                .antMatchers(HttpMethod.GET, "/testing/vip/**").hasRole(role_vip)\
                .antMatchers(HttpMethod.GET, "/testing/admin/**").hasRole(role_admin)\
                .anyRequest().authenticated())\
                .oauth2ResourceServer().jwt().jwtAuthenticationConverter(converter);\
    }\

  因为covert需返回GrantedAuthority格式的队列,因此自定义了一个子类Role。这个子类只做一件事,给role加上ROLE_前缀。

public class Role implements Serializable, GrantedAuthority {\
    private String authority;\
    public Role(Object role){\
        authority = "ROLE_" + (String) role;\
    }

    @Override\
    public String getAuthority() {\
        return authority;\
    }\
}\

  为什么还需要一个自定义Role呢?Token中不是带有已加有前缀的字段了吗?因为在实际部署生产环境的时候,我们更倾向只对一端进行修改,而不是两端同时修改,这也是出于减少依赖,方便日后替换另一端考虑。

  至此,一个适配KeyColaK,支持鉴权的资源服务器就算做好了。

本文内容在Spring boot 2.5.5版本上验证通过

源码 Gitee