Java Spring Spring Security

资源权限表达式

先来解释一下什么叫资源权限表达式。权限控制的核心就是清晰地表达出特定资源的某种操作,一个格式良好好的权限声明可以清晰表达出用户对该资源拥有的操作权限。
通常一个资源在系统中的标识是唯一的,比如User用来标识用户,ORDER标识订单。不管什么资源大都可以归纳出以下这几种操作
Spring Security实现类似Shiro权限表达式的RBAC权限控制 - 图1
在 shiro权限声明通常对上面的这种资源操作关系用冒号分隔的方式进行表示。例如读取用户信息的操作表示为USER:READ,甚至还可以更加细一些,用USER:READ:123表示读取ID为123的用户权限。
资源操作定义好了,再把它和角色关联起来不就是基于RBAC的权限资源控制了吗?就像下面这样:
Spring Security实现类似Shiro权限表达式的RBAC权限控制 - 图2
这样资源和角色的关系可以进行CRUD操作进行动态绑定。

Spring Security中的实现

资源权限表达式的动态权限控制在Spring Security也是可以实现的。首先开启方法级别的注解安全控制。

  1. /**
  2. * 开启方法安全注解
  3. */
  4. @EnableGlobalMethodSecurity(prePostEnabled = true,
  5. securedEnabled = true,
  6. jsr250Enabled = true)
  7. public class MethodSecurityConfig {
  8. }

MethodSecurityExpressionHandler

MethodSecurityExpressionHandler 提供了一个对方法进行安全访问的门面扩展。它的实现类DefaultMethodSecurityExpressionHandler更是提供了针对方法的一系列扩展接口,这里我总结了一下:
Spring Security实现类似Shiro权限表达式的RBAC权限控制 - 图3
这里的PermissionEvaluator正好可以满足需要。

PermissionEvaluator

PermissionEvaluator 接口抽象了对一个用户是否有权限访问一个特定的领域对象的评估过程。

  1. public interface PermissionEvaluator extends AopInfrastructureBean {
  2. boolean hasPermission(Authentication authentication,
  3. Object targetDomainObject, Object permission);
  4. boolean hasPermission(Authentication authentication,
  5. Serializable targetId, String targetType, Object permission);
  6. }

这两个方法仅仅参数列表不同,这些参数的含义为:

  • authentication 当前用户的认证信息,持有当前用户的角色权限。
  • targetDomainObject 用户想要访问的目标领域对象,例如上面的USER。
  • permission 这个当前方法设定的目标领域对象的权限,例如上面的READ。
  • targetId 这种是对上面targetDomainObject 的具体化,比如ID为123的USER,我觉得还可以搞成租户什么的。
  • targetType 是为了配合targetId 。

第一个方法是用来实现USER:READ的;第二个方法是用来实现USER:READ:123的。

思路以及实现

targetDomainObject:permission不就是USER:READ的抽象吗?只要找出USER:READ对应的角色集合,和当前用户持有的角色进行比对,它们存在交集就证明用户有权限访问。借着这个思路实现了一个PermissionEvaluator:

  1. /**
  2. * 资源权限评估
  3. */
  4. public class ResourcePermissionEvaluator implements PermissionEvaluator {
  5. private final BiFunction<String, String, Collection<? extends GrantedAuthority>> permissionFunction;
  6. public ResourcePermissionEvaluator(BiFunction<String, String, Collection<? extends GrantedAuthority>> permissionFunction) {
  7. this.permissionFunction = permissionFunction;
  8. }
  9. @Override
  10. public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
  11. //查询方法标注对应的角色
  12. Collection<? extends GrantedAuthority> resourceAuthorities = permissionFunction.apply((String) targetDomainObject, (String) permission);
  13. // 用户对应的角色
  14. Collection<? extends GrantedAuthority> userAuthorities = authentication.getAuthorities();
  15. // 对比 true 就能访问 false 就不能访问
  16. return userAuthorities.stream().anyMatch(resourceAuthorities::contains);
  17. }
  18. @Override
  19. public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
  20. //todo
  21. System.out.println("targetId = " + targetId);
  22. return true;
  23. }
  24. }

第二个方法没有实现,因为两个差不多,第二个可以想想具体的使用场景。

配置和使用

PermissionEvaluator 需要注入到Spring IoC,并且Spring IoC只能有一个该类型的Bean

  1. @Bean
  2. PermissionEvaluator resourcePermissionEvaluator() {
  3. return new ResourcePermissionEvaluator((targetDomainObject, permission) -> {
  4. //TODO 这里形式其实可以不固定
  5. String key = targetDomainObject + ":" + permission;
  6. //TODO 查询 key 和 authority 的关联关系
  7. // 模拟 permission 关联角色 根据key 去查 grantedAuthorities
  8. Set<SimpleGrantedAuthority> grantedAuthorities = new HashSet<>();
  9. grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
  10. return "USER:READ".equals(key) ? grantedAuthorities : new HashSet<>();
  11. });
  12. }

接下来写个接口,用@PreAuthorize注解标记,然后直接用hasPermission('USER','READ')来静态绑定该接口的访问权限表达式:

  1. @GetMapping("/postfilter")
  2. @PreAuthorize("hasPermission('USER','READ')")
  3. public Collection<String> postfilter(){
  4. List<String> list = new ArrayList<>();
  5. list.add("Fcant");
  6. list.add("Hello");
  7. list.add("World");
  8. return list;
  9. }

然后定义一个用户:

  1. @Bean
  2. UserDetailsService users() {
  3. UserDetails user = User.builder()
  4. .username("Fcant")
  5. .password("123456")
  6. .passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()::encode)
  7. .roles("USER")
  8. .authorities("ROLE_ADMIN","ROLE_USER")
  9. .build();
  10. return new InMemoryUserDetailsManager(user);
  11. }

接下来肯定是正常能够访问接口的。当改变了@PreAuthorize中表达式的值或者移除了用户的ROLE_ADMIN权限,再或者USER:READ关联到了其它角色等等,都会返回403。
可以看看注解改成这样会是什么效果:

  1. @PreAuthorize("hasPermission('USER','READ') or hasRole('ADMIN')")

这样呢?

  1. @PreAuthorize("hasPermission('1234','USER','READ')")

或者让targetId动态化:

  1. @PreAuthorize("hasPermission(#id,'USER','READ')")
  2. public Collection<String> postfilter(String id){
  3. }