在Security中实现Shiro的@RequiresPermissions注解

为何要这样作?

Security和Shiro相信你们都用过,常见的两种权限框架,既然都是属于权限框架,那么确定都有本身的权限控制,为何还要使用Security的同时去实现Shiro的权限控制呢?数据库

因为新项目使用的是Security,因而去百度了一波详解,知道了Security是使用@PreAuthorize注解来实现接口权限控制,app

当咱们在接口上加标注:@PreAuthorize("hasRole('ADMIN')")时,流程大概是这样:框架

先会去调用到User类的getAuthorities接口,取出authorities,类型为List<Role> ,而后调用每一个Role实例的getAuthority接口,该接口返回Role名称,好比“ADMIN”,只要其中某个Role返回了“ADMIN”,便可中止遍历,表示当前用户具有了访问该接口的权限,放行。less

这样在数据库里面就会有user、role、user_role表,分表存储用户信息、角色信息以及用户和角色关联(多对多)信息。若是每一个接口对应一个role,那实际上做为角色的role在咱们看来跟以往的权限(permission)对应了,即其实是user和permission的关系,只是叫作role罢了。因而就会在全部具备ADMIN角色才能访问的接口上加上标注:@PreAuthorize("hasRole('ADMIN')")ui

这样的话,咱们在编写接口代码的时候,就要把这个标注写上去,让具有ADMIN角色的用户能够访问之,那若是某天我不想让ADMIN用户访问这个接口呢,我该怎么办?要么我须要回收该用户的ADMIN角色,要么我得去修改接口标注。若是该角色确实只对应一个接口的权限,那回收却是没有问题,但Boss要你实现一个角色拥有多个权限,实际上会在多个接口上作了一样的标注,回收角色后你会发现其余一些原本该用户能够访问的接口如今访问不了了,头大吧,想来想去你只能去改代码了,去修改某个特定接口的标注。this

因而乎,怎么办,以前有用过Shiro作权限,以为@RequiresPermissions注解很方便,它是针对菜单资源的的接口权限,而且还有Logical.AND(所有包含)和Logical.OR(任意包含)属性,因而决定采用CV大法,实现一波~spa

 

Shiro的@RequiresPermissions的实现

ok,咱们先来看看@RequiresPermissions的实现流程,全部标注了@RequiresPermissions注解的都会进到这里来校验,不经过的话会抛出AuthorizationException异常日志

 

 1 public void assertAuthorized(Annotation a) throws AuthorizationException {
 2     if (!(a instanceof RequiresPermissions)) return; 3 4 RequiresPermissions rpAnnotation = (RequiresPermissions) a; 5 String[] perms = getAnnotationValue(a); 6 Subject subject = getSubject(); 7 8 if (perms.length == 1) { 9 subject.checkPermission(perms[0]); 10 return; 11  } 12 if (Logical.AND.equals(rpAnnotation.logical())) { 13  getSubject().checkPermissions(perms); 14 return; 15  } 16 if (Logical.OR.equals(rpAnnotation.logical())) { 17 // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first 18 boolean hasAtLeastOnePermission = false; 19 for (String permission : perms) if (getSubject().isPermitted(permission)) hasAtLeastOnePermission = true; 20 // Cause the exception if none of the role match, note that the exception message will be a bit misleading 21 if (!hasAtLeastOnePermission) getSubject().checkPermission(perms[0]); 22      (这一行的做用貌似只是为了抛异常?) 23 24  } 25 }

 

 

顺着往下看最终的调用都是implies方法code

 1 //visibility changed from private to protected per SHIRO-332
 2 protected boolean isPermitted(Permission permission, AuthorizationInfo info) {
 3     Collection<Permission> perms = getPermissions(info); 4 if (perms != null && !perms.isEmpty()) { 5 for (Permission perm : perms) { 6 if (perm.implies(permission)) { 7 return true; 8  } 9  } 10  } 11 return false; 12 }

 

拿当前登陆用户的权限循环与@RequirePermissions中的注解对比blog

 1 public boolean implies(Permission p) {
 2     // By default only supports comparisons with other WildcardPermissions
 3     if (!(p instanceof WildcardPermission)) { 4 return false; 5  } 6 7 WildcardPermission wp = (WildcardPermission) p; 8 9 List<Set<String>> otherParts = wp.getParts(); 10 11 int i = 0; 12 for (Set<String> otherPart : otherParts) { 13 // If this permission has less parts than the other permission, everything after the number of parts contained 14 // in this permission is automatically implied, so return true 15 if (getParts().size() - 1 < i) { 16 return true; 17 } else { 18 Set<String> part = getParts().get(i); 19 if (!part.contains(WILDCARD_TOKEN) && !part.containsAll(otherPart)) { 20 return false; 21  } 22 i++; 23  } 24  } 25 26 // If this permission has more parts than the other parts, only imply it if all of the other parts are wildcards 27 for (; i < getParts().size(); i++) { 28 Set<String> part = getParts().get(i); 29 if (!part.contains(WILDCARD_TOKEN)) { 30 return false; 31  } 32  } 33 34 return true; 35 }

 

代码实现

既然知道了实现流程,那么咱们开启CV大法,本身实现一个。

首先,自定义注解@PermissionCheck(默认是所有包含),搞里头~

 1 // 标注这个类它能够标注的位置
 2 @Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
 3 // 标注这个注解的注解保留时期
 4 @Retention(RetentionPolicy.RUNTIME) 5 // 是否生成注解文档 6 @Documented 7 public @interface PermissionCheck { 8 9  String[] value(); 10 11 Logical logical() default Logical.AND; 12 }

 

 

Logical枚举类,搞里头~

public enum Logical {
    AND, OR
}

 

 

而后定义一个拦截器PermissionCheckAspect,搞里头~

@Aspect
@Component
@Slf4j
public class PermissionCheckAspect { //切入点表达式决定了用注解方式的方法切仍是针对某个路径下的全部类和方法进行切,方法必须是返回void类型 @Pointcut(value = "@annotation(com.cn.tianxia.admin.base.annotation.PermissionCheck)") private void permissionCheckCut(){}; //定义了切面的处理逻辑。即方法上加了@PermissionCheck @Around("permissionCheckCut()") public Object around(ProceedingJoinPoint pjp) throws Throwable{ Signature signature = pjp.getSignature(); SecurityUser user = SecurityAuthorHolder.getSecurityUser(); //角色权限校验 MethodSignature methodSignature = (MethodSignature)signature; Method targetMethod = methodSignature.getMethod(); if (targetMethod.isAnnotationPresent(PermissionCheck.class)){ //获取方法上注解中代表的权限 PermissionCheck permission = targetMethod.getAnnotation(PermissionCheck.class); Logical logical = permission.logical(); //获取权限注解value,可能有多个 String[] permissionArr = permission.value(); //取出用户拥有的权限 List<String> permsList = user.getMenus().stream().map(SysMenu::getPerms).distinct().collect(Collectors.toList()); //取出permsList和permissionArr的交集  permsList.retainAll(Arrays.asList(permissionArr)); /** AND处理(彻底包含) OR处理(任意包含)**/ if(Logical.AND.equals(logical)){ if(permsList.size() == permissionArr.length){ return pjp.proceed(); } }else{ if(permsList.size() > 0){ return pjp.proceed(); } } //非法操做 记录日志信息 log.error("非法操做!当前接口请求的用户={},访问路径={}",user.getLoginName(),Arrays.asList(permissionArr).toString()); } return RR.exception("无权调用接口!"); } }

 

 

使用

@PermissionCheck(value = {"user:info","user:edit"},logical = Logical.OR)
@PostMapping(value = "/getUserInfo", produces = BaseConsts.REQUEST_HEADERS_CONTENT_TYPE) @ApiOperation(value = "用户管理-获取用户信息", notes = "用户管理-获取用户信息", httpMethod = BaseConsts.REQUEST_METHOD, response = RR.class) public RR getUserInfo() throws Exception { ...... }

 

 参考来源:https://www.nndev.cn/archives/869

 

总结 : 接口权限验证无非就是过滤器和拦截器实现,因为定义拦截器是环切,也能够将@PermissionCheck定义在Service层,即Controller入口是一个,Service是动态的。

相关文章
相关标签/搜索