Spring Boot + Spring Cloud 實現許可權管理系統 後端篇(二十四):許可權控制(Shiro 註解)
線上演示
演示地址:ofollow,noindex" target="_blank">http://139.196.87.48:9002/kitty
使用者名稱:admin 密碼:admin
技術背景
當前,我們基於導航選單的顯示和操作按鈕的禁用狀態,實現了頁面可見性和操作可用性的許可權驗證,或者叫訪問控制。但這僅限於頁面的顯示和操作,我們的後臺介面還是沒有進行許可權的驗證,只要知道了後臺的介面資訊,就可以直接通過swagger或自行傳送ajax請求成功呼叫後臺介面,這是非常危險的。接下來,我們就基於Shiro的註解式許可權控制方案,來給我們的後臺介面提供許可權保護。
許可權註解
Shiro總共有5個許可權註解,實現了不同的許可權控制策略。
RequiresPermissions
當前Subject需要擁有某些特定的許可權時,才能執行被該註解標註的方法。如果當前Subject不具有這樣的許可權,則方法不會被執行。
這是基於資源許可權方式的許可權控制主要方案,也是我們專案中進行許可權控制使用的註解方案。
RequiresRoles
當前Subject必須擁有所有指定的角色時,才能訪問被該註解標註的方法。如果當天Subject不同時擁有所有指定角色,則方法不會執行還會丟擲AuthorizationException異常。
RequiresUser
當前Subject必須是應用的使用者,才能訪問或呼叫被該註解標註的類,例項,方法。
RequiresAuthentication
使用該註解標註的類,例項,方法在訪問或呼叫時,當前Subject必須在當前session中已經過認證。
RequiresGuest
使用該註解標註的類,例項,方法在訪問或呼叫時,當前Subject可以是“gust”身份,不需要經過認證或者在原先的session中存在記錄。
註解優先順序
Shiro的認證註解處理具有內定處理順序,如有多個註解,會按照下面優先順序逐個檢查,只有所有檢查通過才允許訪問:
- RequiresRoles
- RequiresPermissions
- RequiresAuthentication
- RequiresUser
- RequiresGuest
程式碼實現
新增配置
開啟kitty-admin工程,找到shiro配置類。新增如下內容,主要作用是開啟Shiro的許可權註解。
Shiro通過AOP方式攔截被許可權註解的類或方法,然後匹配許可權註解值和使用者許可權列表進行驗證。
ShiroConfig.java
/** * Shiro生命週期處理器 */ @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * 開啟Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證 * 配置以下兩個bean(DefaultAdvisorAutoProxyCreator(可選)和AuthorizationAttributeSourceAdvisor)即可實現此功能 */ @Bean @DependsOn({"lifecycleBeanPostProcessor"}) public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager()); return authorizationAttributeSourceAdvisor; }
添加註解
以選單管理介面為例,新增 @RequiresPermissions("許可權標識") 標識即可。
這個許可權標識就是我們的選單表中對應的許可權標識欄位(perms)對應的值。
SysMenuController.java
package com.louis.kitty.admin.controller; import java.util.List; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.louis.kitty.admin.model.SysMenu; import com.louis.kitty.admin.sevice.SysMenuService; import com.louis.kitty.core.http.HttpResult; /** * 選單控制器 * @author Louis * @date Oct 29, 2018 */ @RestController @RequestMapping("menu") public class SysMenuController { @Autowired private SysMenuService sysMenuService; @RequiresPermissions({"sys:menu:add", "sys:menu:edit"}) @PostMapping(value="/save") public HttpResult save(@RequestBody SysMenu record) { return HttpResult.ok(sysMenuService.save(record)); } @RequiresPermissions("sys:menu:delete") @PostMapping(value="/delete") public HttpResult delete(@RequestBody List<SysMenu> records) { return HttpResult.ok(sysMenuService.delete(records)); } @RequiresPermissions("sys:menu:view") @GetMapping(value="/findNavTree") public HttpResult findNavTree(@RequestParam String userName) { return HttpResult.ok(sysMenuService.findTree(userName, 1)); } @RequiresPermissions("sys:menu:view") @GetMapping(value="/findMenuTree") public HttpResult findMenuTree() { return HttpResult.ok(sysMenuService.findTree(null, 0)); } }
測試效果
啟動服務,通過Swagger分別使用超級管理員和測試人員角色賬戶訪問介面,發現admin可以正常訪問,無許可權的賬戶訪問返回如下許可權驗證失敗資訊。
{ "timestamp": "2018-11-19T07:58:21.532+0000", "status": 500, "error": "Internal Server Error", "message": "Subject does not have permission [sys:menu:view]", "path": "/menu/findMenuTree" }
原理剖析
首先在Shiro配置的時候,我們配置了一個 AuthorizationAttributeSourceAdvisor 類。
/** * Shiro生命週期處理器 */ @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * 開啟Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證 * 配置以下兩個bean(DefaultAdvisorAutoProxyCreator(可選)和AuthorizationAttributeSourceAdvisor)即可實現此功能 */ @Bean @DependsOn({"lifecycleBeanPostProcessor"}) public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager()); return authorizationAttributeSourceAdvisor; }
在 AuthorizationAttributeSourceAdvisor 類中,我們看到了有關五個許可權註解的資訊,以及關聯一個攔截器 AopAllianceAnnotationsAuthorizingMethodInterceptor。
public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES = new Class[] { RequiresPermissions.class, RequiresRoles.class, RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class }; ... public AuthorizationAttributeSourceAdvisor() { setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor()); } }
在 AopAllianceAnnotationsAuthorizingMethodInterceptor 中,我們看到了關聯了五種許可權控制註解物件的攔截器,這樣在添加了許可權註解的方法被呼叫時,就會被對應的攔截器攔截,並進行相關的許可權驗證。
public class AopAllianceAnnotationsAuthorizingMethodInterceptor extends AnnotationsAuthorizingMethodInterceptor implements MethodInterceptor { public AopAllianceAnnotationsAuthorizingMethodInterceptor() { List<AuthorizingAnnotationMethodInterceptor> interceptors = new ArrayList<AuthorizingAnnotationMethodInterceptor>(5); //use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the //raw JDK resolution process. AnnotationResolver resolver = new SpringAnnotationResolver(); //we can re-use the same resolver instance - it does not retain state: interceptors.add(new RoleAnnotationMethodInterceptor(resolver)); interceptors.add(new PermissionAnnotationMethodInterceptor(resolver)); interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver)); interceptors.add(new UserAnnotationMethodInterceptor(resolver)); interceptors.add(new GuestAnnotationMethodInterceptor(resolver)); setMethodInterceptors(interceptors); }
介面被呼叫時,AOP攔截器 AopAllianceAnnotationsAuthorizingMethodInterceptor 的invoke方法被呼叫。
public Object invoke(MethodInvocation methodInvocation) throws Throwable { org.apache.shiro.aop.MethodInvocation mi = createMethodInvocation(methodInvocation); return super.invoke(mi); }
呼叫父類 AuthorizingMethodInterceptor 的 invoke 方法。
public Object invoke(MethodInvocation methodInvocation) throws Throwable { assertAuthorized(methodInvocation); return methodInvocation.proceed(); }
呼叫 AopAllianceAnnotationsAuthorizingMethodInterceptor 的 assertAuthorized 方法。
protected void assertAuthorized(MethodInvocation methodInvocation) throws AuthorizationException { //default implementation just ensures no deny votes are cast: Collection<AuthorizingAnnotationMethodInterceptor> aamis = getMethodInterceptors(); if (aamis != null && !aamis.isEmpty()) { for (AuthorizingAnnotationMethodInterceptor aami : aamis) { if (aami.supports(methodInvocation)) { aami.assertAuthorized(methodInvocation); } } } }
呼叫 AuthorizingAnnotationMethodInterceptor 的 assertAuthorized 方法。
public void assertAuthorized(MethodInvocation mi) throws AuthorizationException { try { ((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi)); } catch(AuthorizationException ae) { ... } }
呼叫 PermissionAnnotationHandler 的 assertAuthorized 方法。
public void assertAuthorized(Annotation a) throws AuthorizationException { if (!(a instanceof RequiresPermissions)) return; RequiresPermissions rpAnnotation = (RequiresPermissions) a; String[] perms = getAnnotationValue(a); Subject subject = getSubject(); if (perms.length == 1) { subject.checkPermission(perms[0]); return; } ... }
呼叫 DelegatingSubject 的 checkPermission方法。
public void checkPermission(String permission) throws AuthorizationException { assertAuthzCheckPossible(); securityManager.checkPermission(getPrincipals(), permission); }
呼叫 AuthorizingSecurityManager 的 checkPermission方法。
public void checkPermission(PrincipalCollection principals, String permission) throws AuthorizationException { this.authorizer.checkPermission(principals, permission); }
呼叫 ModularRealmAuthorizer 的 checkPermission方法。
public void checkPermission(PrincipalCollection principals, String permission) throws AuthorizationException { assertRealmsConfigured(); if (!isPermitted(principals, permission)) { throw new UnauthorizedException("Subject does not have permission [" + permission + "]"); } }
public boolean isPermitted(PrincipalCollection principals, String permission) { assertRealmsConfigured(); for (Realm realm : getRealms()) { if (!(realm instanceof Authorizer)) continue; if (((Authorizer) realm).isPermitted(principals, permission)) { return true; } } return false; }
呼叫 AuthorizingRealm 的 isPermitted方法。
public boolean isPermitted(PrincipalCollection principals, String permission) { Permission p = getPermissionResolver().resolvePermission(permission); return isPermitted(principals, p); }
public boolean isPermitted(PrincipalCollection principals, Permission permission) { AuthorizationInfo info = getAuthorizationInfo(principals); return isPermitted(permission, info); }
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) { ... if (info == null) { // Call template method if the info was not found in a cache info = doGetAuthorizationInfo(principals); ... } return info; }
呼叫我們自定義的 OAuth2Realm 的 doGetAuthorizationInfo 方法,也是返回自定義許可權驗證的邏輯。
/** * 授權(介面保護,驗證介面呼叫許可權時呼叫) */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SysUser user = (SysUser)principals.getPrimaryPrincipal(); // 使用者許可權列表,根據使用者擁有的許可權標識與如 @permission標註的介面對比,決定是否可以呼叫介面 Set<String> permsSet = sysUserService.findPermissions(user.getName()); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.setStringPermissions(permsSet); return info; }
AuthorizingRealm 查詢到使用者許可權資訊,將註解許可權值跟使用者許可權資訊列表進行匹配,決定許可權驗證是否通過。
protected boolean isPermitted(Permission permission, AuthorizationInfo info) { Collection<Permission> perms = getPermissions(info); if (perms != null && !perms.isEmpty()) { for (Permission perm : perms) { if (perm.implies(permission)) { return true; } } } return false; }
到這裡,關於Shiro註解式許可權控制方案的配置和執行流程就剖析的差不多了。
原始碼下載
後端:https://gitee.com/liuge1988/kitty
前端:https://gitee.com/liuge1988/kitty-ui.git
作者:朝雨憶輕塵
出處:https://www.cnblogs.com/xifengxiaoma/
版權所有,歡迎轉載,轉載請註明原文作者及出處。