Spring Security怎么给你授权的?

简介: Spring Security核心功能, 认证和授权, 本章便是核心章节, 授权, 需要关注, 关注, 再关注授权是什么?

前言
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配合的动态权限方案中, 角色的继承将会失效, 因为你只需要在数据库中修改对应角色的权限就可以修改权限了, 不过这也是后续的事情, 后面会详细介绍

相关文章
|
5月前
|
安全 Java 数据安全/隐私保护
使用Spring Security实现细粒度的权限控制
使用Spring Security实现细粒度的权限控制
|
5月前
|
安全 Java 数据库
实现基于Spring Security的权限管理系统
实现基于Spring Security的权限管理系统
|
5月前
|
安全 Java 数据安全/隐私保护
解析Spring Security中的权限控制策略
解析Spring Security中的权限控制策略
|
6月前
|
JSON 安全 Java
Spring Security 6.x 微信公众平台OAuth2授权实战
上一篇介绍了OAuth2协议的基本原理,以及Spring Security框架中自带的OAuth2客户端GitHub的实现细节,本篇以微信公众号网页授权登录为目的,介绍如何在原框架基础上定制开发OAuth2客户端。
218 4
Spring Security 6.x 微信公众平台OAuth2授权实战
|
4月前
|
Java Spring
Spring Boot Admin 授权配置
Spring Boot Admin 授权配置
31 0
|
5月前
|
安全 Java 数据安全/隐私保护
使用Spring Security实现细粒度的权限控制
使用Spring Security实现细粒度的权限控制
|
5月前
|
安全 Java 数据安全/隐私保护
使用Java和Spring Security实现身份验证与授权
使用Java和Spring Security实现身份验证与授权
|
5月前
|
存储 安全 Java
Spring Security在企业级应用中的应用
Spring Security在企业级应用中的应用
|
6月前
|
存储 安全 Java
Spring Security与OAuth2集成开发
Spring Security与OAuth2集成开发
|
6月前
|
存储 安全 Java
Spring Boot中的OAuth2认证与授权
Spring Boot中的OAuth2认证与授权
下一篇
无影云桌面