为什么要这样做?
Security和Shiro相信大家都用过,常见的两种权限框架,既然都是属于权限框架,那么肯定都有自己的权限控制,为什么还要使用Security的同时去实现Shiro的权限控制呢?
由于新项目使用的是Security,于是去百度了一波详解,知道了Security是使用@PreAuthorize注解来实现接口权限控制,
当我们在接口上加标注:@PreAuthorize("hasRole('ADMIN')")
时,流程大概是这样:
先会去调用到User类的getAuthorities接口,取出authorities,类型为List
这样在数据库里面就会有user、role、user_role表,分表存储用户信息、角色信息以及用户和角色关联(多对多)信息。如果每个接口对应一个role,那实际上作为角色的role在我们看来跟以往的权限(permission)对应了,即实际上是user和permission的关系,只是叫做role罢了。于是就会在所有具有ADMIN角色才能访问的接口上加上标注:@PreAuthorize("hasRole('ADMIN')")
这样的话,我们在编写接口代码的时候,就要把这个标注写上去,让具备ADMIN角色的用户可以访问之,那如果某天我不想让ADMIN用户访问这个接口呢,我该怎么办?要么我需要回收该用户的ADMIN角色,要么我得去修改接口标注。如果该角色确实只对应一个接口的权限,那回收倒是没有问题,但Boss要你实现一个角色拥有多个权限,实际上会在多个接口上做了同样的标注,回收角色后你会发现其他一些本来该用户可以访问的接口现在访问不了了,头大吧,想来想去你只能去改代码了,去修改某个特定接口的标注。
于是乎,怎么办,之前有用过Shiro做权限,觉得@RequiresPermissions注解很方便,它是针对菜单资源的的接口权限,并且还有Logical.AND(全部包含)和Logical.OR(任意包含)属性,于是决定采用CV大法,实现一波~
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方法
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中的注解对比
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是动态的。