Spring Security怎么给你授权的?

8,462

前言

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中权限和角色,是同一个东西都是字符串啊, 只不过会给角色前面加上前缀以示区分而已, 给你看张图"

image-20230112135812104

小黑: "这是一张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<String> authorities;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    return AuthorityUtils.createAuthorityList(authorities.toArray(new String[0]));
}

小黑: "直接从数据库中查出所有的权限,然后在这个方法中直接返回就行了,就这么简单。"

小黑: "如果他是用户 <==> 角色 <==> 权限 <==> 资源这种情况"

// 这里只代表当前角色, 角色可以有多种, 但是在我给的事例代码中需要用户手动切换角色
// private Role role;

// 这种表示拿出用户的所有角色
private List<Role> 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函数"

image-20230111190329778

角色继承

Spring Security提供了用户角色权限继承功能, 比如你是班主任也是老师, 那么班主任可以继承老师角色的权限, 并提供属于班主任的权限

这里只是举个例子, 不要杠精哦

小白: "等等, 你不是说 Spring Security没有角色么?"

小黑: "你中文肯定不合格, 我说的是Spring Security代码层面角色和权限没区别, 都是字符串而已, 而非没有角色"

Spring Security中通过RoleHierarchy接口实现角色继承功能

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

image-20230111191233244

RoleHierarchy中只有一个getReachableGrantedAuthorities方法,该方法返回用户真正“可触达”的权限。

举个简单例子,假设用户定义了ROLE_ADMIN继承自 ROLE_USERROLE_USER继承自ROLE_GUEST,现在当前用户角色是ROLE_ADMIN,但是它实际可访问的资源也包含ROLE_USERROLE_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_USERROLE_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<GrantedAuthority> 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<GrantedAuthority> 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<GrantedAuthority> 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);
   }

}

最终添加的结果是这样:

image-20221202162341432

至此分组彻底完成

roleHierarchy.getReachableGrantedAuthorities
@Override
public Collection<GrantedAuthority> getReachableGrantedAuthorities(
      Collection<? extends GrantedAuthority> authorities) {
   if (authorities == null || authorities.isEmpty()) {
      return AuthorityUtils.NO_AUTHORITIES;
   }
   Set<GrantedAuthority> reachableRoles = new HashSet<>();
   // 从参数拿到的角色名被存入到下面的函数 ROLE_ADMIN
   Set<String> 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);
}

最后结果:

image-20221202163159109

结果返回到这样了:

image-20221202163239489

至此源码分析完成

总结下:

整个过程, 像是借助我们的表达式, 解析出我们写入的字符串的表达式, 存放在 Map<String, Set<GrantedAuthority>> rolesReachableInOneStepMap对象

但该集合中的内容是不完整的, rolesReachableInOneStepMap集合只能存放一级角色关系, 比如你是 admin , 那么该集合只能存放到 user 这个级别, 不能存放 guest 这个级别

接着就是搜索可达角色, 存放在这个集合中Map<String, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap

可达搜索的关键在于 !this.rolesReachableInOneStepMap.containsKey(lowerRole.getAuthority())判断. 如果该返回为 true, 则直接 continue, 返回 false 的话直接到this.rolesReachableInOneOrMoreStepsMap.put(roleName, visitedRolesSet);

上面的整个过程也非常简单, 如果 ROLE_ADMIN 的成员有一个 ROLE_USER, 然后在rolesReachableInOneStepMap分组中判断下 ROLE_USER 是否为组长, 如果是组长, 则意味着 ROLE_ADMINROLE_USER 的组长, 所以 ROLE_USER 的成员都是 ROLE_ADMIN

小白: "有问题, 看角色继承的源码"

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

授权的方式

授权的方式有两种

  1. 基于过滤器(URL)FilterSecurityInterceptor
  2. 基于AOP(方法)MethodSecurityInterceptor

image-20230111192617929

小白: "只有这两种?"

小黑: "是的, 就这两种, 根据颗粒度不同处理不同的资源"

授权过程源码分析

这里的源码分析我将会分为两种,也就是上面的两种不同的颗粒度: 根据URL和根据方法。

首先我们会根据URL进行源码分析,然后再根据方法分析。

首先我们需要确定授权过程的核心接口都有哪一些。

image-20230112150906458

在这里有一个核心的属性需要关注一下。

image-20230112150940641

这个属性就是一个集合,这跟前面认证是差不多的,认证过程中Manager也是一个集合, 而Provider是集合元素。

从这个属性我们可以看到一个接口,这个就是投票器接口。

image-20230112151306608

image-20230112151523071这里看这个也非常简单,是吧?

现在就是第3个核心接口。

public interface ConfigAttribute extends Serializable {
   String getAttribute();
}

这里面存放的都是权限或者是权限所需要的表达式。

image-20230112152130836

源码分析思路

现在看这三个核心接口你大概也就知道他怎么玩了,说白了就是一个manager集合,然后遍历这个集合,里面集合的元素是投票器,每一个投票器都有一个投票方法和匹配方法,匹配方法通过匹配ConfigAttribute, 来判断使用哪一个投票器

接着投票器调用投票方法,在投票方法中遍历当前用户的权限目标资源的权限看看当前用户的权限是否满足访问资源的权限。根据遍历的结果,是否匹配返回三种不同的结果,

  • 第1种是允许访问
  • 第2种是弃权
  • 第3种是拒绝

对应着接口的三个常量

image-20230112152045460

紧接着我们就可以根据我们所分析出来的思路进行源码跟踪。

分析源码一定要带着目的去, 否则分析不出什么东西的

源码分析

第1步从根源开始。就是匹配到底是URL还是方法?

AbstractSecurityInterceptor

image-20230112152346304

此次源码分析大致涉及这几个类

image-20230112162236651

从URL开始分析

FilterSecurityInterceptor

image-20230112152441880

这个类其实很眼熟。

在spring security的过滤器列表中的倒二还是倒三就有一个,上面这个类。

202211130451077

202211130452065

image-20230112165714261

image-20230112170318921

可以发现这里有三个核心方法, 根据这三个方法可以看到整个源码分析的过程

AbstractSecurityInterceptor#beforeInvocation

protected InterceptorStatusToken beforeInvocation(Object object) {
   Assert.notNull(object, "Object was null");
   if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
      throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName()
            + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
            + getSecureObjectClass());
   }
    // 获取资源(也就是object)所需的权限(和角色)
   Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
    // 资源没有对应的权限, 直接返回 null
   if (CollectionUtils.isEmpty(attributes)) {
      publishEvent(new PublicInvocationEvent(object));
      return null; // no further work post-invocation
   }
    // 没有认证信息, 直接抛出异常
   if (SecurityContextHolder.getContext().getAuthentication() == null) {
      credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
            "An Authentication object was not found in the SecurityContext"), object, attributes);
   }
    // 从SecurityContextHolder中拿到Authentication
   Authentication authenticated = authenticateIfRequired();
   // 准备调用决策器, 后面方法不用分析了
   attemptAuthorization(object, attributes, authenticated);
   if (this.logger.isDebugEnabled()) {
      this.logger.debug(LogMessage.format("Authorized %s with attributes %s", object, attributes));
   }
   if (this.publishAuthorizationSuccess) {
      publishEvent(new AuthorizedEvent(object, attributes, authenticated));
   }

   // Attempt to run as a different user
   Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
   if (runAs != null) {
      SecurityContext origCtx = SecurityContextHolder.getContext();
      SecurityContext newCtx = SecurityContextHolder.createEmptyContext();
      newCtx.setAuthentication(runAs);
      SecurityContextHolder.setContext(newCtx);

      if (this.logger.isDebugEnabled()) {
         this.logger.debug(LogMessage.format("Switched to RunAs authentication %s", runAs));
      }
      // need to revert to token.Authenticated post-invocation
      return new InterceptorStatusToken(origCtx, true, attributes, object);
   }
   this.logger.trace("Did not switch RunAs authentication since RunAsManager returned null");
   // 返回成果, 但由于是基于过滤器的方式, 不需要处理后续的两个函数
    // 所以这里返回之后的函数就不需要分析了, 主要分析上面的attemptAuthorization函数
   return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);

}
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
      Authentication authenticated) {
   try {
      this.accessDecisionManager.decide(authenticated, object, attributes);
   }
    // 省略一堆代码
}
// AffirmativeBased
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
      throws AccessDeniedException {
   int deny = 0;
    // 集合遍历投票器
   for (AccessDecisionVoter voter : getDecisionVoters()) {
       // 投票, 产生结果
      int result = voter.vote(authentication, object, configAttributes);
      switch (result) {
      case AccessDecisionVoter.ACCESS_GRANTED:
         return;
      case AccessDecisionVoter.ACCESS_DENIED:
         deny++;
         break;
      default:
         break;
      }
   }
    // 如果没事还好基本上投票通过了, 否则会抛出AccessDeniedException异常
   if (deny > 0) {
      throw new AccessDeniedException(
            this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
   }
   // To get this far, every AccessDecisionVoter abstained
   checkAllowIfAllAbstainDecisions();
}

接下来就是给猪都能编写的代码了

// RoleVoter
@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
   if (authentication == null) {
      return ACCESS_DENIED;
   }
   int result = ACCESS_ABSTAIN;
   Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
   for (ConfigAttribute attribute : attributes) {
      if (this.supports(attribute)) {
         result = ACCESS_DENIED;
         for (GrantedAuthority authority : authorities) {
            if (attribute.getAttribute().equals(authority.getAuthority())) {
               return ACCESS_GRANTED;
            }
         }
      }
   }
   return result;
}

像不像在集合 [1, 2, 3][3] 中遍历是否有相同的元素代码?

只不过前者是资源所需权限数组, 后者是当前用户所有的权限数组

来, 给个绝杀流程图

流程图可能有错, 但大体上没什么问题

Spring Security授权源码分析

至此基于URL过滤器方式的源码分析基本完毕

对了, 源码分析过程在这个项目中进行的

dynamic-permission-demo

数据库表结构也在那里面

image-20230113023329908

从AOP(方法)方式开始分析

image-20230113025326322

可以看出是基于Spring AOP实现的拦截功能

拦截后直接断在这里

image-20230113025434158

还是熟悉的三个函数

这里我就直接给出流程图吧, 源码给出的太多, 看我写的注释也很累, 直接看流程图快些

Spring Security授权基于AOP

基本分析源代码和URL差不多, 只不过在处理权限时存在不同之处, 还有那三个函数的最后一个函数afterInvocation在 AOP 方式中还是有用途的

主要目的就是处理后置注解, 比如 @PostAuthorize @PostFilter

方式也非常简单, 根据一个条件, 判断是否是后置处理注解, 然后看 AfterInvocationManagerAfterInvocationProvider 这两接口

Manager是集合, Provider是集合的元素, 核心方法还是 decide , 只不过 Provider 多了个 findPostInvocationAttribute 函数

该函数的功能也非常简单, 判断前置还是后置注解

private PostInvocationAttribute findPostInvocationAttribute(Collection<ConfigAttribute> config) {
   for (ConfigAttribute attribute : config) {
      if (attribute instanceof PostInvocationAttribute) {
         return (PostInvocationAttribute) attribute;
      }
   }
   return null;
}

对应着还有一个检测是否是前置注解的函数, 不过不在 Provider 中了

PreInvocationAuthorizationAdviceVoter 类中, 看类名可以判断出来是处理前置注解的

不管是不是后置注解, 都会执行一次 PreInvocationAuthorizationAdviceVoter, 只不过在该类的findPostInvocationAttribute函数中判断出来不是前置注解才停下来的

private PreInvocationAttribute findPreInvocationAttribute(Collection<ConfigAttribute> config) {
   for (ConfigAttribute attribute : config) {
      if (attribute instanceof PreInvocationAttribute) {
         return (PreInvocationAttribute) attribute;
      }
   }
   return null;
}

image-20230113040614824

这两接口都继承至ConfigAttribute

我前面说的 findPreInvocationAttribute 判断是的前置注解还是后置注解是不对的, 这里判断的其实是权限类型, 到底是前置还是后置

更多的源码看流程图吧, 打字好累

对了最后再给出投票器的类族介绍吧

image-20221130203443286

  • RoleVoter: RoleVoter是根据登录主体的角色进行投票,即判断当前用户是否具备受保护对象所需要的角色。需要注意的是,默认情况下角色需以“ROLE_”开始,否则 supports方法直接返回false,不进行后续的投票操作。
  • RoleHierarchyVoter: RoleHierarchyVoter继承自RoleVoter,投票逻辑和RoleVoter一致,不同的是RoleHierarchyVoter支持角色的继承,它通过RoleHierarchylmpl对象对用户所具有的角色进行解析,获取用户真正“可触达”的角色;而 RoleVoter则直接调用authentication.getAuthorities()方法获取用户的角色。
  • WebExpressionVoter:基于URL地址进行权限控制时的投票器(支持SpEL)。
  • Jsr250Voter:处理JSR-250权限注解的投票器,如@PermitAll@DenyAll等。
  • AuthenticatedVoter: AuthenticatedVoter用于判断当前用户的认证形式,它有三种取值:IS_AUTHENTICATED_FULLYIS_AUTHENTICATED_REMEMBERED以及IS_AUTHENTICATED_ANONYMOUSLY。其中:IS_AUTHENTICATED_FULLY 要求当前用户既不是匿名用户也不是通过RememberMe进行认证 ;IS_AUTHENTICATED_REMEMBERED则在前者的基础上,允许用户通过RememberMe进行认证;IS_AUTHENTICATED_ANONYMOUSLY则允许当前用户通过RememberMe认证,也允许当前用户是匿名用户。
  • AbstractAclVoter:基于ACL进行权限控制时的投票器。这是一个抽象类,没有绑定到具体的ACL系统(关于ACL,后面会做详细介绍))。
  • AclEntryVoter: AcIEntryVoter继承自AbstractAclVoter,基于Spring Security提供的ACL权限系统的投票器。
  • PreInvocationAuthorizationAdviceVoter: 处理@PreFilter@PreAuthorize注解的投票器。

投票结果并非最终结果(通过或拒绝),最终结果还要看决策器(AccessDecisionManager)。

分析完源码我们可以发现有个扩展点, 该扩展点是 SecurityMetadataSource

image-20230113041039116

我们知道, 目标资源的权限除了在 Spring Security上配置外, 还可以通过动态权限方式在数据库中配置, 所以上面这个方法明显需要修改, 修改成从数据库获取权限列表

否则将无法从数据库中抓取目标资源的权限集合

不过本篇字数已经 6000了, 太多字各位读者看着也累, 我手也写的好酸, 放在下一篇吧

总结

对了最后再强调一遍, 角色继承和动态权限有冲突, 如果我们的权限 角色 用户 都从数据库读取的话, 那么角色继承将会失效, 这点需要注意, 都能从数据库中修改了, 何必还搞什么角色继承

紧接着就是 授权的方式, 一个是 基于过滤器, 另一个是基于 AOP 实现, 说白了, 一个是 URL 粗颗粒度, 另一个是方法, 细颗粒度

在 Spring Security中 角色和权限只不过是字符串, 一部分朋友给出的建议是 Authentiation.getAuthorities函数(换句话说就是我们自定义的 User类的 getAuthorities 函数)返回权限, 而我给出的建议是返回角色和权限

小白: "对了, 还有个问题, 那就是如果你的系统有角色, 那么你要怎么设计表结构呢? 让Spring Secuiry多出关于角色表的操作?"

小黑: "其实只需要在 mybatis 和 自定义的User类 中操作就好, 至于你说的角色表, 就更简单了, 可以直接看我的sql模板, 至于角色对象, 看下面代码"

@Data
public class Role implements GrantedAuthority {
   // 这是角色名
   private String name;
   // 这是权限, 多个权限
   private List<SimpleGrantedAuthority> allowedOperations = new ArrayList<>();
   
   // 获得角色名
   @Override
   public String getAuthority() {
      return this.name;
   }
}

授权过程也非常简单, 主要核心就是拿到当前用户所持有的权限和目标资源的权限 做遍历匹配完事, 还记得那个一头猪都能写出的两个 for 循环么? 底层就是这么的直接, 然后根据是否匹配而产生三种状态, 根据这三种状态在决策器中执行只要保证不抛出异常就表示成功, 意味着当前用户有权限访问该资源, 如果抛出异常, 表示当前用户没有权限访问该资源

待续