答案是能!
松哥之前写过相似的文章,然而次要是讲了用法,明天咱们来看看原理!
本文基于以后 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";}
}
这三个测试接口,咱们的布局是这样的:
- /hello 是任何人都能够拜访的接口
- /admin/hello 是具备 admin 身份的人才能拜访的接口
- /user/hello 是具备 user 身份的人才能拜访的接口
- 所有 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 家族中应用十分宽泛,它的匹配规定也非常简单:
通配符 | 含意 |
---|---|
** |
匹配多层门路 |
* |
匹配一层门路 |
? |
匹配任意单个字符 |
下面配置的含意是:
- 如果申请门路满足
/admin/**
格局,则用户须要具备 admin 角色。 - 如果申请门路满足
/user/**
格局,则用户须要具备 user 角色。 - 残余的其余格局的申请门路,只须要认证(登录)后就能够拜访。
留神代码中配置的三条规定的程序十分重要,和 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
三个接口,其中:
/hello
因为登录后就能够拜访,这个接口拜访胜利。/admin/hello
须要 admin 身份,所以拜访失败。/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_A
、ROLE_B
以及 ROLE_C
。
getReachableGrantedAuthorities 办法的目标就是是依据角色档次定义,将用户真正能够触达的角色解析进去。
RoleHierarchy 接口有两个实现类,如下图:
- NullRoleHierarchy 这是一个空的实现,将传入的参数一成不变返回。
- RoleHierarchyImpl 这是咱们上文所应用的实现,这个会实现一些解析操作。
咱们来重点看下 RoleHierarchyImpl 类。
这个类中实际上就四个办法 setHierarchy
、getReachableGrantedAuthorities
、buildRolesReachableInOneStepMap
以及 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. 小结
好啦,明天就和小伙伴们简简单单聊一下角色继承的问题,感兴趣的小伙伴能够本人试一下~如果感觉有播种,记得点个在看激励下松哥哦~