前言
Spring Security核心功能, 认证和授权, 本章便是核心章节, 授权, 需要关注, 关注, 再关注
授权是什么?
首先到底什么是授权, 通俗易懂版:
你有什么权限以支持你去做哪些事, 操作哪些资源
认证和授权是怎么配合工作的?
小白: "前面我们知道, 认证成功之后会将数据存储在SecurityContextHolder上下文中, 那么这些用户信息怎么在授权阶段使用?"
小黑: "在 Spring Security 中认证和授权是完全分开的关系, 不管你认证使用的是Basic Http认证还是Disgest Http认证方式还是基于表单的认证方式, 都不影响我后续的授权, 这一点你需要记住"
小白: "那你还是没有说到关键点"
小黑: "嗯, 请看这里"
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
}
复制代码
小黑: "Spring Security围绕上面的getAuthorities函数完成授权, 就这么简单, 你当然登录的用户有什么权限, 就看上面这个函数返回的集合有什么权限了"
小白: "不对啊, 我角色呢? 现在大家不都是基于角色访问控制(Role-Based Access Control)么?"
小黑: "是啊, 这个问题需要具体讨论"
是角色还是权限?
在Spring Security的代码层面看, 角色和权限没有很大的区别, 只能说权限和角色在Spring Security层面都只不过是字符串而已, 角色前面多了个ROLE_来区分是不是角色, 但这两都是字符串
小白: "等等, 问个问题, 角色和权限有什么区别?"
小黑: "角色你可以看做是权限的集合, 当然他两的关系是多对多关系, 区别还是权限和角色的颗粒度不同, 权限你可以看做是原子, 而角色你可以看做是分子"
小白: "那么从Authentication.getAuthorities函数拿出来的集合是角色还是权限?"
小黑: "getAuthorities函数为什么不能同时返回角色和权限呢? 两个一起返回"
小白: "啊? 那不乱么?"
小黑: "前面不是说了吗,在spring security中权限和角色,是同一个东西都是字符串啊, 只不过会给角色前面加上前缀以示区分而已, 给你看张图"
小黑: "这是一张Spring Secuirty默认生成的表结构, 看看里面的内容, 包括角色和权限"
小黑: "该方法的返回值一般是这样的,比如说你要查询叫zhazha这个用户的权限或者角色,那么它会返回这样一个集合'ROLE_ADMIN, readHello, writeHello', 这个函数就是这样用的。"
小白: "等等, 有些用户有多个角色,那你应该拿到哪一个角色的权限集合呢?还是两个角色的权限集合全部拿到呢?"
小黑: "这个需要根据你系统的设计而决定,正常情况下呢,如果你在登录完成之后,有一个切换角色的按钮,那么在这样的一个系统中,你应该拿到单个角色的权限集合。如果你的系统没有切换角色这个按钮,那么应该返回所有角色的所有权限集合。"
小黑: "为了更好理解我,把两种模式的user对象代码列出来。"
public class Users implements Serializable, UserDetails, CredentialsContainer {
private Long id;
private String username;
private String password;
private Boolean enabled = true;
// 省略一堆属性
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
}
复制代码
小黑: "如果他是用户 <==> 权限 <==> 资源这种情况。"
private List authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return AuthorityUtils.createAuthorityList(authorities.toArray(new String[0]));
}
复制代码
小黑: "直接从数据库中查出所有的权限,然后在这个方法中直接返回就行了,就这么简单。"
小黑: "如果他是用户 <==> 角色 <==> 权限 <==> 资源这种情况"
// 这里只代表当前角色, 角色可以有多种, 但是在我给的事例代码中需要用户手动切换角色
// private Role role;
// 这种表示拿出用户的所有角色
private List roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (CollUtil.isEmpty(roles)) {
return Collections.emptyList();
}
// 添加角色名到 authorities 中
final List authorities = new ArrayList<>(AuthorityUtils.createAuthorityList(roles.stream().map(Role::getAuthority).collect(Collectors.joining())));
roles.forEach(role -> {
if (CollUtil.isNotEmpty(role.getOperations())) {
// 添加权限到 authorities 中
authorities.addAll(AuthorityUtils.createAuthorityList(role.getOperations().stream().map(Operation::getAuthority).collect(Collectors.joining())));
}
});
return authorities;
}
复制代码
小白: "这样不是每次授权都要访问一次数据库么? "
小黑: "你忘了么? 这里是User对象下面的getAuthorities函数, 而不是Authentication.getAuthorities函数, 而Authentication是缓存在session中的(当然有些情况缓存在redis中)"
小黑: "User对象的getAuthorities函数只会在下面代码执行一次, 之后就被缓存在session中了, 而授权并非使用的User对象, 而是 Authentication对象下面的getAuthorities函数"
角色继承
Spring Security提供了用户角色权限继承功能, 比如你是班主任也是老师, 那么班主任可以继承老师角色的权限, 并提供属于班主任的权限
这里只是举个例子, 不要杠精哦
小白: "等等, 你不是说 Spring Security没有角色么?"
小黑: "你中文肯定不合格, 我说的是Spring Security代码层面角色和权限没区别, 都是字符串而已, 而非没有角色"
Spring Security中通过RoleHierarchy接口实现角色继承功能
public interface RoleHierarchy {
Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(
Collection<? extends GrantedAuthority> authorities);
}
复制代码
RoleHierarchy中只有一个getReachableGrantedAuthorities方法,该方法返回用户真正“可触达”的权限。
举个简单例子,假设用户定义了ROLE_ADMIN继承自 ROLE_USER,ROLE_USER继承自ROLE_GUEST,现在当前用户角色是ROLE_ADMIN,但是它实际可访问的资源也包含ROLE_USER和ROLE_GUEST能访问的资源.
getReachableGrantedAuthorities方法就是根据当前用户所具有的角色,从角色层级映射中解析出用户真正“可触达”的权限。
RoleHierarchy只有一个实现类RoleHierarchyImpl(还有一个没啥用的实现类),开发者一般通过RoleHierarchyImpl类来定义角色的层级关系,如下面代码表示:
@Test
void roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER > ROLE_GUEST");
System.err.println(roleHierarchy.getReachableGrantedAuthorities(List.of(new SimpleGrantedAuthority("ROLE_USER")))); // [ROLE_USER, ROLE_GUEST]
System.err.println(roleHierarchy.getReachableGrantedAuthorities(List.of(new SimpleGrantedAuthority("ROLE_GUEST")))); // [ROLE_GUEST]
System.err.println(roleHierarchy.getReachableGrantedAuthorities(List.of(new SimpleGrantedAuthority("ROLE_ADMIN")))); // [ROLE_USER, ROLE_GUEST, ROLE_ADMIN]
}
复制代码
说白了, 就是一个分组对象
在项目中一般这么用?
@Configuration
public class RoleConfig {
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER > ROLE_GUEST");
return roleHierarchy;
}
}
复制代码
我感觉也不是很方便的样子
源码分析
这段源码分析是必须的, 晚上一堆配置方法, 但对于角色继承方法来说, 是有新旧方法之分的, 所以我们需要事先声明, 我们的版本是基于 Spring Boot 2.7.5
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER > ROLE_GUEST");
return roleHierarchy;
}
复制代码
我们基于这段代码分析源码
首先我们进入的函数是这个:
public void setHierarchy(String roleHierarchyStringRepresentation) {
// 保存 ROLE_ADMIN > ROLE_USER > ROLE_GUEST
this.roleHierarchyStringRepresentation = roleHierarchyStringRepresentation;
buildRolesReachableInOneStepMap();
buildRolesReachableInOneOrMoreStepsMap();
}
复制代码
然后分为这两个方法buildRolesReachableInOneStepMap buildRolesReachableInOneOrMoreStepsMap
buildRolesReachableInOneStepMap
private void buildRolesReachableInOneStepMap() {
this.rolesReachableInOneStepMap = new HashMap<>();
// 首先对字符串进行 \n 分组, 然后遍历, 我们的代码少了 \n ROLE_ADMIN > ROLE_USER > ROLE_GUEST
for (String line : this.roleHierarchyStringRepresentation.split("\n")) {
// 对 ' > '(大括号前面有空格, 至少一个空格) 进行分组
String[] roles = line.trim().split("\\s+>\\s+");
// i = 1, 只对前面两个角色有处理
for (int i = 1; i < roles.length; i++) {
// 拿出第一个 ROLE_ADMIN 虽然 i = 1 但是拿的是第 0 个
String higherRole = roles[i - 1];
// 拿到下一个角色, ROLE_USER
GrantedAuthority lowerRole = new SimpleGrantedAuthority(roles[i]);
Set<GrantedAuthority> rolesReachableInOneStepSet;
// 不包含 ROLE_ADMIN
if (!this.rolesReachableInOneStepMap.containsKey(higherRole)) {
rolesReachableInOneStepSet = new HashSet<>();
// 以 ROLE_ADMIN 为 key, new 出 value
this.rolesReachableInOneStepMap.put(higherRole, rolesReachableInOneStepSet);
}
else {
rolesReachableInOneStepSet = this.rolesReachableInOneStepMap.get(higherRole);
}
// 将 ROLE_USER 添加到 ROLE_ADMIN 分组底下
rolesReachableInOneStepSet.add(lowerRole);
}
}
}
复制代码
上面的代码, 针对 ROLE_ADMIN 的分组, 添加了 ROLE_USER
然后在内部的那个 for 循环中, 对 ROLE_USER 创建了一个分组, 然后在 分组中添加 ROLE_GUEST
最终结果是
ROLE_ADMIN 分组只有 ROLE_USER 的权限
ROLE_USER 分组有 ROLE_GUEST权限
所以这么写, 最后的结果 原本 应该拥有 ROLE_USER 和 ROLE_GUEST 权限的 ROLE_ADMIN 只能有 ROLE_USER , 没有ROLE_GUEST的权限
如果我们没看roleHierarchy.getReachableGrantedAuthorities方法的话, 这肯定是不对的
这里其实三种方法都行
@Test
public void test03() throws Exception {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
StrJoiner joiner = StrJoiner.of("\n").append("ROLE_ADMIN > ROLE_USER").append("ROLE_USER > ROLE_GUEST").append("ROLE_ADMIN > ROLE_GUEST");
roleHierarchy.setHierarchy(joiner.toString());
Collection authorityCollection = roleHierarchy.getReachableGrantedAuthorities(List.of(() -> "ROLE_ADMIN"));
for (GrantedAuthority grantedAuthority : authorityCollection) {
System.out.print(grantedAuthority.getAuthority() + "\t"); // ROLE_USER ROLE_GUEST ROLE_ADMIN
}
}
@Test
public void test02() throws Exception {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER > ROLE_GUEST");
Collection authorityCollection = roleHierarchy.getReachableGrantedAuthorities(List.of(() -> "ROLE_ADMIN"));
for (GrantedAuthority grantedAuthority : authorityCollection) {
System.out.print(grantedAuthority.getAuthority() + "\t"); // ROLE_USER ROLE_GUEST ROLE_ADMIN
}
}
@Test
void test01() throws Exception {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
StringJoiner stringJoiner = new StringJoiner("\n");
stringJoiner.add("ROLE_ADMIN > ROLE_USER");
stringJoiner.add("ROLE_USER > ROLE_GUEST");
roleHierarchy.setHierarchy(stringJoiner.toString());
Collection authorityCollection = roleHierarchy.getReachableGrantedAuthorities(List.of(() -> "ROLE_ADMIN"));
for (GrantedAuthority grantedAuthority : authorityCollection) {
System.out.print(grantedAuthority.getAuthority() + "\t"); // ROLE_USER ROLE_GUEST ROLE_ADMIN
}
}
复制代码
buildRolesReachableInOneOrMoreStepsMap
给可达的分组继续添加剩余的角色
我们分析源码的过程完全按照"ROLE_ADMIN > ROLE_USER > ROLE_GUEST"分析的
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);
// 将拿到的成员添加到 visitedRolesSet 集合中, 添加成功, 继续下一次循环
// 核心代码在!this.rolesReachableInOneStepMap.containsKey(lowerRole.getAuthority())
// 如果添加的 ROLE_ADMIN 组长的成员 ROLE_USER 在原先分组中也担任组长的话, 那意味着 ROLE_USER 组长底下的所有成员也是 ROLE_ADMIN 的成员
// 因为 ROLE_ADMIN 也是 ROLE_USER 的成员
// 所以下面的那个 !containsKey(不包含) 方法, 不执行
if (!visitedRolesSet.add(lowerRole)
|| !this.rolesReachableInOneStepMap.containsKey(lowerRole.getAuthority())) {
continue; // Already visited role or role with missing hierarchy
}
else if (roleName.equals(lowerRole.getAuthority())) {
throw new CycleInRoleHierarchyException();
}
// 将搜索到的所有可达成员, 添加到新的集合的分组中, 换句话说就是new了个新的分组, 在分组 ROLE_USER 下, 添加可达成员 ROLE_GUEST
// 如果是 ROLE_ADMIN 分组组长, 那么就添加 ROLE_USER 和 ROLE_GUEST
// !containsKey 不包含代码不执行 continue 之后, 就会将 ROLE_USER 底下的所有成员都给 ROLE_ADMIN
// 往这个集合 rolesToVisitSet 添加另一个集合后, 上面的 !rolesToVisitSet.isEmpty() 条件也满足了, 继续添加 ROLE_USER 的成员
rolesToVisitSet.addAll(this.rolesReachableInOneStepMap.get(lowerRole.getAuthority()));
}
// 将上面的结果visitedRolesSet 添加到 roleName 的分组中
this.rolesReachableInOneOrMoreStepsMap.put(roleName, visitedRolesSet);
}
}
复制代码
最终添加的结果是这样:
至此分组彻底完成
roleHierarchy.getReachableGrantedAuthorities
@Override
public Collection getReachableGrantedAuthorities(
Collection<? extends GrantedAuthority> authorities) {
if (authorities == null || authorities.isEmpty()) {
return AuthorityUtils.NO_AUTHORITIES;
}
Set reachableRoles = new HashSet<>();
// 从参数拿到的角色名被存入到下面的函数 ROLE_ADMIN
Set processedNames = new HashSet<>();
for (GrantedAuthority authority : authorities) {
// Do not process authorities without string representation
if (authority.getAuthority() == null) {
reachableRoles.add(authority);
continue;
}
// processedNames.add("ROLE_ADMIN")
if (!processedNames.add(authority.getAuthority())) {
continue;
}
// Add original authority
reachableRoles.add(authority);
// 从这里拿到可达集合, 根据数组 ROLE_ADMIN 拿到组长的成员 ROLE_USER 和 ROLE_GUEST
Set<GrantedAuthority> lowerRoles = this.rolesReachableInOneOrMoreStepsMap.get(authority.getAuthority());
if (lowerRoles == null) {
continue; // No hierarchy for the role
}
for (GrantedAuthority role : lowerRoles) {
// 添加已经添加了 (ROLE_ADMIN) , 现在准备添加 ROLE_USER 和 ROLE_GUEST
if (processedNames.add(role.getAuthority())) {
// 将对象也添加到可达列表中(ROLE_ADMIN, ROLE_USER 和 ROLE_GUEST)
reachableRoles.add(role);
}
}
}
return new ArrayList<>(reachableRoles);
}
复制代码
最后结果:
结果返回到这样了:
至此源码分析完成
总结下:
整个过程, 像是借助我们的表达式, 解析出我们写入的字符串的表达式, 存放在 Map<String, Set> rolesReachableInOneStepMap对象
但该集合中的内容是不完整的, rolesReachableInOneStepMap集合只能存放一级角色关系, 比如你是 admin , 那么该集合只能存放到 user 这个级别, 不能存放 guest 这个级别
接着就是搜索可达角色, 存放在这个集合中Map<String, Set> rolesReachableInOneOrMoreStepsMap
可达搜索的关键在于 !this.rolesReachableInOneStepMap.containsKey(lowerRole.getAuthority())判断. 如果该返回为 true, 则直接 continue, 返回 false 的话直接到this.rolesReachableInOneOrMoreStepsMap.put(roleName, visitedRolesSet);
上面的整个过程也非常简单, 如果 ROLE_ADMIN 的成员有一个 ROLE_USER, 然后在rolesReachableInOneStepMap分组中判断下 ROLE_USER 是否为组长, 如果是组长, 则意味着 ROLE_ADMIN 是ROLE_USER 的组长, 所以 ROLE_USER 的成员都是 ROLE_ADMIN 的
小白: "有问题, 看角色继承的源码"
public Collection getReachableGrantedAuthorities(
Collection<? extends GrantedAuthority> authorities)
复制代码
小白: "看这个函数里面不是返回了角色和权限吗?但是这个函数其实他只要角色就行了,这样把权限传进去不会有问题吗?"
小黑: "没有任何的影响"
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 this.roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities());
}
}
复制代码
小黑: "看看上面的代码,然后我再写下面这个测试案例。"
@Test
public void test04() throws Exception {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER > ROLE_GUEST");
Collection authorities = roleHierarchy.getReachableGrantedAuthorities(AuthorityUtils.createAuthorityList("ROLE_ADMIN", "readHello", "writeHello"));
for (GrantedAuthority grantedAuthority : authorities) {
log.error(grantedAuthority.getAuthority() + "\t"); // ROLE_USER ROLE_GUEST writeHello ROLE_ADMIN readHello
}
}
复制代码
小黑: "下面注释的地方就是本次测试案例的执行结果。所以不管传递角色还是权限,都不会影响到角色继承的结果呈现。"
记住, 在以sql配合的动态权限方案中, 角色的继承将会失效, 因为你只需要在数据库中修改对应角色的权限就可以修改权限了, 不过这也是后续的事情, 后面会详细介绍