技術背景
當前,我們基於導航菜單的顯示和操作按鈕的禁用狀態,實現了頁面可見性和操作可用性的權限驗證,或者叫訪問控制。但這僅限於頁面的顯示和操作,我們的后台接口還是沒有進行權限的驗證,只要知道了后台的接口信息,就可以直接通過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注解式權限控制方案的配置和執行流程就剖析的差不多了。

