关于java:Spring-Security-中如何让上级拥有下级的所有权限

12次阅读

共计 7511 个字符,预计需要花费 19 分钟才能阅读完成。

答案是能!

松哥之前写过相似的文章,然而次要是讲了用法,明天咱们来看看原理!

本文基于以后 Spring Security 5.3.4 来剖析,为什么要强调最新版呢?因为在在 5.0.11 版中,角色继承配置和当初不一样。旧版的计划咱们当初不探讨了,间接来看以后最新版是怎么解决的。

1. 角色继承案例

咱们先来一个简略的权限案例。

创立一个 Spring Boot 我的项目,增加 Spring Security 依赖,并创立两个测试用户,如下:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.inMemoryAuthentication()
            .withUser("javaboy")
            .password("{noop}123").roles("admin")
            .and()
            .withUser("江南一点雨")
            .password("{noop}123")
            .roles("user");
}

而后筹备三个测试接口,如下:

@RestController
public class HelloController {@GetMapping("/hello")
    public String hello() {return "hello";}

    @GetMapping("/admin/hello")
    public String admin() {return "admin";}

    @GetMapping("/user/hello")
    public String user() {return "user";}
}

这三个测试接口,咱们的布局是这样的:

  1. /hello 是任何人都能够拜访的接口
  2. /admin/hello 是具备 admin 身份的人才能拜访的接口
  3. /user/hello 是具备 user 身份的人才能拜访的接口
  4. 所有 user 可能拜访的资源,admin 都可能拜访

留神第四条标准意味着所有具备 admin 身份的人主动具备 user 身份。

接下来咱们来配置权限的拦挡规定,在 Spring Security 的 configure(HttpSecurity http) 办法中,代码如下:

http.authorizeRequests()
        .antMatchers("/admin/**").hasRole("admin")
        .antMatchers("/user/**").hasRole("user")
        .anyRequest().authenticated()
        .and()
        ...
        ...

这里的匹配规定咱们采纳了 Ant 格调的门路匹配符,Ant 格调的门路匹配符在 Spring 家族中应用十分宽泛,它的匹配规定也非常简单:

通配符 含意
** 匹配多层门路
* 匹配一层门路
? 匹配任意单个字符

下面配置的含意是:

  1. 如果申请门路满足 /admin/** 格局,则用户须要具备 admin 角色。
  2. 如果申请门路满足 /user/** 格局,则用户须要具备 user 角色。
  3. 残余的其余格局的申请门路,只须要认证(登录)后就能够拜访。

留神代码中配置的三条规定的程序十分重要,和 Shiro 相似,Spring Security 在匹配的时候也是依照从上往下的程序来匹配,一旦匹配到了就不持续匹配了, 所以拦挡规定的程序不能写错

如果应用角色继承,这个性能很好实现,咱们只须要在 SecurityConfig 中增加如下代码来配置角色继承关系即可:

@Bean
RoleHierarchy roleHierarchy() {RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
    hierarchy.setHierarchy("ROLE_admin > ROLE_user");
    return hierarchy;
}

留神,在配置时,须要给角色手动加上 ROLE_ 前缀。下面的配置示意 ROLE_admin 主动具备 ROLE_user 的权限。

接下来,咱们启动我的项目进行测试。

我的项目启动胜利后,咱们首先以 江南一点雨的身份进行登录:

登录胜利后,别离拜访 /hello/admin/hello 以及 /user/hello 三个接口,其中:

  1. /hello 因为登录后就能够拜访,这个接口拜访胜利。
  2. /admin/hello 须要 admin 身份,所以拜访失败。
  3. /user/hello 须要 user 身份,所以拜访胜利。

再以 javaboy 身份登录,登录胜利后,咱们发现 javaboy 也能拜访 /user/hello 这个接口了,阐明咱们的角色继承配置没问题!

2. 原理剖析

这里配置的外围在于咱们提供了一个 RoleHierarchy 实例,所以咱们的剖析就从该类动手。

RoleHierarchy 是一个接口,该接口中只有一个办法:

public interface RoleHierarchy {
    Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(Collection<? extends GrantedAuthority> authorities);

}

这个办法参数 authorities 是一个权限汇合,从办法名上看办法的返回值是一个可拜访的权限汇合。

举个简略的例子,假如角色层次结构是 ROLE_A > ROLE_B > ROLE_C,当初间接给用户调配的权限是 ROLE_A,但实际上用户领有的权限有 ROLE_AROLE_B 以及 ROLE_C

getReachableGrantedAuthorities 办法的目标就是是依据角色档次定义,将用户真正能够触达的角色解析进去。

RoleHierarchy 接口有两个实现类,如下图:

  • NullRoleHierarchy 这是一个空的实现,将传入的参数一成不变返回。
  • RoleHierarchyImpl 这是咱们上文所应用的实现,这个会实现一些解析操作。

咱们来重点看下 RoleHierarchyImpl 类。

这个类中实际上就四个办法 setHierarchygetReachableGrantedAuthoritiesbuildRolesReachableInOneStepMap 以及 buildRolesReachableInOneOrMoreStepsMap,咱们来一一进行剖析。

首先是咱们一开始调用的 setHierarchy 办法,这个办法用来设置角色层级关系:

public void setHierarchy(String roleHierarchyStringRepresentation) {
    this.roleHierarchyStringRepresentation = roleHierarchyStringRepresentation;
    if (logger.isDebugEnabled()) {logger.debug("setHierarchy() - The following role hierarchy was set:"
                + roleHierarchyStringRepresentation);
    }
    buildRolesReachableInOneStepMap();
    buildRolesReachableInOneOrMoreStepsMap();}

用户传入的字符串变量设置给 roleHierarchyStringRepresentation 属性,而后通过 buildRolesReachableInOneStepMap 和 buildRolesReachableInOneOrMoreStepsMap 办法实现对角色层级的解析。

buildRolesReachableInOneStepMap 办法用来将角色关系解析成一层一层的模式。咱们来看下它的源码:

private void buildRolesReachableInOneStepMap() {this.rolesReachableInOneStepMap = new HashMap<>();
    for (String line : this.roleHierarchyStringRepresentation.split("\n")) {String[] roles = line.trim().split("\\s+>\\s+");
        for (int i = 1; i < roles.length; i++) {String higherRole = roles[i - 1];
            GrantedAuthority lowerRole = new SimpleGrantedAuthority(roles[i]);
            Set<GrantedAuthority> rolesReachableInOneStepSet;
            if (!this.rolesReachableInOneStepMap.containsKey(higherRole)) {rolesReachableInOneStepSet = new HashSet<>();
                this.rolesReachableInOneStepMap.put(higherRole, rolesReachableInOneStepSet);
            } else {rolesReachableInOneStepSet = this.rolesReachableInOneStepMap.get(higherRole);
            }
            rolesReachableInOneStepSet.add(lowerRole);
        }
    }
}

首先大家看到,依照换行符来解析用户配置的多个角色层级,这是什么意思呢?

咱们后面案例中只是配置了 ROLE_admin > ROLE_user,如果你须要配置多个继承关系,怎么配置呢?多个继承关系用 \n 隔开即可,如下 ROLE_A > ROLE_B \n ROLE_C > ROLE_D。还有一种状况,如果角色层级关系是间断的,也能够这样配置 ROLE_A > ROLE_B > ROLE_C > ROLE_D

所以这里先用 \n 将多层继承关系拆离开造成一个数组,而后对数组进行遍历。

在具体遍历中,通过 > 将角色关系拆分成一个数组,而后对数组进行解析,高一级的角色作为 key,低一级的角色作为 value。

代码比较简单,最终的解析进去存入 rolesReachableInOneStepMap 中的层级关系是这样的:

假如角色继承关系是 ROLE_A > ROLE_B \n ROLE_C > ROLE_D \n ROLE_C > ROLE_E,Map 中的数据是这样:

  • A–>B
  • C–>[D,E]

假如角色继承关系是 ROLE_A > ROLE_B > ROLE_C > ROLE_D,Map 中的数据是这样:

  • A–>B
  • B–>C
  • C–>D

这是 buildRolesReachableInOneStepMap 办法解析进去的 rolesReachableInOneStepMap 汇合。

接下来的 buildRolesReachableInOneOrMoreStepsMap 办法则是对 rolesReachableInOneStepMap 汇合进行再次解析,将角色的继承关系拉平。

例如 rolesReachableInOneStepMap 中保留的角色继承关系如下:

  • A–>B
  • B–>C
  • C–>D

通过 buildRolesReachableInOneOrMoreStepsMap 办法解析之后,新的 Map 中保留的数据如下:

  • A–>[B、C、D]
  • B–>[C、D]
  • C–>D

这样解析实现后,每一个角色能够触达到的角色就高深莫测了。

咱们来看下 buildRolesReachableInOneOrMoreStepsMap 办法的实现逻辑:

private void buildRolesReachableInOneOrMoreStepsMap() {this.rolesReachableInOneOrMoreStepsMap = new HashMap<>();
    for (String roleName : this.rolesReachableInOneStepMap.keySet()) {Set<GrantedAuthority> rolesToVisitSet = new HashSet<>(this.rolesReachableInOneStepMap.get(roleName));
        Set<GrantedAuthority> visitedRolesSet = new HashSet<>();
        while (!rolesToVisitSet.isEmpty()) {GrantedAuthority lowerRole = rolesToVisitSet.iterator().next();
            rolesToVisitSet.remove(lowerRole);
            if (!visitedRolesSet.add(lowerRole) ||
                    !this.rolesReachableInOneStepMap.containsKey(lowerRole.getAuthority())) {continue;} else if (roleName.equals(lowerRole.getAuthority())) {throw new CycleInRoleHierarchyException();
            }
            rolesToVisitSet.addAll(this.rolesReachableInOneStepMap.get(lowerRole.getAuthority()));
        }
        this.rolesReachableInOneOrMoreStepsMap.put(roleName, visitedRolesSet);
    }
}

这个办法还比拟奇妙。首先依据 roleName 从 rolesReachableInOneStepMap 中获取对应的 rolesToVisitSet,这个 rolesToVisitSet 是一个 Set 汇合,对其进行遍历,将遍历后果增加到 visitedRolesSet 汇合中,如果 rolesReachableInOneStepMap 汇合的 key 不蕴含以后读取进去的 lowerRole,阐明这个 lowerRole 就是整个角色体系中的最底层,间接 continue。否则就把 lowerRole 在 rolesReachableInOneStepMap 中对应的 value 拿进去持续遍历。

最初将遍历后果存入 rolesReachableInOneOrMoreStepsMap 汇合中即可。

这个办法有点绕,小伙伴们能够本人打个断点品一下。

看了下面的剖析,小伙伴们可能发现了,其实角色继承,最终还是拉平了去比照。

咱们定义的角色有层级,然而代码中又将这种层级拉平了,不便后续的比对。

最初还有一个 getReachableGrantedAuthorities 办法,依据传入的角色剖析出其可能潜在蕴含的一些角色:

public Collection<GrantedAuthority> getReachableGrantedAuthorities(Collection<? extends GrantedAuthority> authorities) {if (authorities == null || authorities.isEmpty()) {return AuthorityUtils.NO_AUTHORITIES;}
    Set<GrantedAuthority> reachableRoles = new HashSet<>();
    Set<String> processedNames = new HashSet<>();
    for (GrantedAuthority authority : authorities) {if (authority.getAuthority() == null) {reachableRoles.add(authority);
            continue;
        }
        if (!processedNames.add(authority.getAuthority())) {continue;}
        reachableRoles.add(authority);
        Set<GrantedAuthority> lowerRoles = this.rolesReachableInOneOrMoreStepsMap.get(authority.getAuthority());
        if (lowerRoles == null) {continue;}
        for (GrantedAuthority role : lowerRoles) {if (processedNames.add(role.getAuthority())) {reachableRoles.add(role);
            }
        }
    }
    List<GrantedAuthority> reachableRoleList = new ArrayList<>(reachableRoles.size());
    reachableRoleList.addAll(reachableRoles);
    return reachableRoleList;
}

这个办法的逻辑比拟直白,就是从 rolesReachableInOneOrMoreStepsMap 汇合中查问出以后角色真正可拜访的角色信息。

3.RoleHierarchyVoter

getReachableGrantedAuthorities 办法将在 RoleHierarchyVoter 投票器中被调用。

public class RoleHierarchyVoter extends RoleVoter {
    private RoleHierarchy roleHierarchy = null;
    public RoleHierarchyVoter(RoleHierarchy roleHierarchy) {Assert.notNull(roleHierarchy, "RoleHierarchy must not be null");
        this.roleHierarchy = roleHierarchy;
    }
    @Override
    Collection<? extends GrantedAuthority> extractAuthorities(Authentication authentication) {
        return roleHierarchy.getReachableGrantedAuthorities(authentication
                .getAuthorities());
    }
}

对于 Spring Security 投票器,将是另外一个故事,松哥将在下篇文章中和小伙伴们分享投票器和决策器~

4. 小结

好啦,明天就和小伙伴们简简单单聊一下角色继承的问题,感兴趣的小伙伴能够本人试一下~如果感觉有播种,记得点个在看激励下松哥哦~

正文完
 0