在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是動態的。

相關文章
相關標籤/搜索