Spring Boot + Spring Cloud 實現權限管理系統 後端篇(二十四):權限控制(Shiro 註解)

在線演示

演示地址:http://139.196.87.48:9002/kitty前端

用戶名:admin 密碼:adminjava

技術背景

當前,咱們基於導航菜單的顯示和操做按鈕的禁用狀態,實現了頁面可見性和操做可用性的權限驗證,或者叫訪問控制。但這僅限於頁面的顯示和操做,咱們的後臺接口仍是沒有進行權限的驗證,只要知道了後臺的接口信息,就能夠直接經過swagger或自行發送ajax請求成功調用後臺接口,這是很是危險的。接下來,咱們就基於Shiro的註解式權限控制方案,來給咱們的後臺接口提供權限保護。git

權限註解

Shiro總共有5個權限註解,實現了不一樣的權限控制策略。web

RequiresPermissions

當前Subject須要擁有某些特定的權限時,才能執行被該註解標註的方法。若是當前Subject不具備這樣的權限,則方法不會被執行。ajax

這是基於資源權限方式的權限控制主要方案,也是咱們項目中進行權限控制使用的註解方案。spring

RequiresRoles

當前Subject必須擁有全部指定的角色時,才能訪問被該註解標註的方法。若是當天Subject不一樣時擁有全部指定角色,則方法不會執行還會拋出AuthorizationException異常。apache

RequiresUser

當前Subject必須是應用的用戶,才能訪問或調用被該註解標註的類,實例,方法。後端

RequiresAuthentication

使用該註解標註的類,實例,方法在訪問或調用時,當前Subject必須在當前session中已通過認證。安全

RequiresGuest

使用該註解標註的類,實例,方法在訪問或調用時,當前Subject能夠是「gust」身份,不須要通過認證或者在原先的session中存在記錄。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/ 版權全部,歡迎轉載,轉載請註明原文做者及出處。

相關文章
相關標籤/搜索