基於shiro的自定義註解的擴充套件

Cross__發表於2018-08-09

基於shiro的自定義註解的擴充套件

根據我的上一篇文章,許可權設計的雜談中,涉及到了有關於前後端分離中,頁面和api介面斷開表與表層面的關聯,另闢蹊徑從其他角度找到方式進行關聯。這裡我們主要採取了shiro的自定義註解的方案。本篇文章主要解決以下的問題。

  1. 如何通過邏輯進行頁面與api介面的關聯。
  2. shiro的自身註解的用法。
  3. 如何編寫自定義註解。

如何通過邏輯進行頁面與api介面的關聯

在表與表的結構關係中,頁面和介面表最終都是與許可權表進行的關聯(詳情請檢視我的上一篇文章《許可權設計的雜談》)。

許可權實體圖
我們現在希望用另一種方案去替代他,實現一個低成本同時兼顧一定程度的許可權控制。這裡我們引入兩個概念。業務模組操作型別

  • 業務模組
    • 概念:將系統中的業務模組抽象成一種資料,我們可以用字串的形式去表示,例如:角色管理對應是role-manage、使用者管理對應是user-manage等等。我們將系統中所存在的業務模組通過“最小特權原則”進行劃分,最終形成一批可分配的資料。
    • 使用原則:api介面和頁面以及功能從本質上來說,都和業務模組有邏輯關係,於是,我們可以對api介面與頁面(以及功能點)進行邏輯匹配,來判斷頁面與介面的關係。
  • 操作型別
    • 概念:將系統中的所有的操作型別抽象成一種資料,我們也可以用字串的形式去表示,例如:新增對應的是add、分配對應的是allot等等。我們將系統中所有的操作型別根據業務模組通過“資料許可證”進行劃分,最終形成一批可分配的資料。
    • 使用原則:頁面是展示,功能點是動作,而介面是最終動作的資源提供,通過“業務模組”確定了調取的資源,通過“操作型別”確定了資源的使用方式。通過兩者可以大致無誤的判斷頁面的功能點觸發的介面是否在鑑權之內。

現在提出了這兩個概念,他們最終的實際的使用方式是什麼,我們先從以下幾個角度去思考一下。

  1. 資料庫中的頁面表或的api介面表中的資料就是真實有效嗎?
  2. 頁面或介面的實際使用,是以功能存在為前提,還是以資料庫表中的資料存在為前提。
  3. 許可權結構中,“控制物件”的儲存只有資料庫這一種途徑嗎?

我們從結論出發來看這幾個問題,首先“控制物件”的儲存除了在資料庫中也可以程式碼中,也可以在配置檔案中,並不一定非得在資料庫;那麼接著回答第二個問題,當資料庫存在的介面資訊,而服務端並沒有開發這個介面的時候,資料庫的信本身就有問題,亦或者,資料庫裡新增的介面必定是服務端上已經部署的介面才能生效;接著就是第一個問題,那麼資料庫中關於“控制物件”的表中的資料並不一定是真實有效的。所以我們可以得出以下的解決方案

  1. 我們可以在介面上用註解的形式補充“業務模組”和“操作型別”的資料資訊,這兩類資訊都可以存於常量類中,
  2. 在資料庫新增建立頁面表結構和頁面功能表結構的時候,新增“業務模組”和“操作型別”欄位。
  3. 可以將“業務模組”和“操作型別”的資訊存於資料庫的字典表中。
  4. 模組的新增或操作的新增,必定帶來了介面的新增,那麼就會帶來一次系統部署活動,這個運維成本是無法減少的,並不能通過表結構來減少。

業務模組與頁面和介面的關係

但是這種方案僅適用於非強控制介面型的專案,在強控制型的介面專案仍然要將頁面與介面進行繫結,雖然這會帶來巨大的運維成本。另外也可以通過介面路由規則進行劃分,例如:/api/page/xxxx/(僅對頁面使用),/api/mobile/xxxxx(僅對移動端使用)將僅供頁面使用的介面進行分類,這類介面僅做認證不做授權,也可以達到目的。

shiro的自身註解的用法

通過一個理論上的思路認可之後,剩下的則是付諸技術上的實踐,我們這邊採用的是Apache Shiro的安全框架,在Spring Boot的環境下應用。簡要說明以下幾個shiro的註解。

註解名 作用
@RequiresAuthentication 作用於的類、方法、例項上。呼叫時,當前的subject是必須經過了認證的。
@RequiresGuest 作用於的類、方法、例項上。呼叫時,subject可以是guest狀態。
@RequiresPermissions 作用於的類、方法、例項上。呼叫時,需要判斷suject中是否包含當前介面中的Permission(許可權資訊)。
@RequiresRoles 作用於的類、方法、例項上。呼叫時,需要判斷subject中是否包含當前介面中的Role(角色資訊)。
@RequiresUser 作用於的類、方法、例項上。呼叫時,需要判斷subject中是否當前應用中的使用者。
    /**
     * 1.當前介面需要經過"認證"過程
     * @return
     */
    @RequestMapping(value = "/info",method = RequestMethod.GET)
    @RequiresAuthentication
    public String test(){
        return "恭喜你,拿到了引數資訊";
    }
    
    /**
     * 2.1.當前介面需要經過許可權校驗(需包含 角色的查詢 或 選單的查詢)
     * @return
     */
    @RequestMapping(value = "/info",method = RequestMethod.GET)
    @RequiresPermissions(value={"role:search","menu":"search"},logical=Logical.OR)
    public String test(){
        return "恭喜你,拿到了引數資訊";
    }
    
    /**
     * 2.2.當前介面需要經過許可權校驗(需包含 角色的查詢 與 選單的查詢)
     * @return
     */
    @RequestMapping(value = "/info",method = RequestMethod.GET)
    @RequiresPermissions(value={"role:search","menu":"search"},logical=Logical.OR)
    public String test(){
        return "恭喜你,拿到了引數資訊";
    }
    
    /**
     * 3.1.當前介面需要經過角色校驗(需包含admin的角色)
     * @return
     */
    @RequestMapping(value = "/info",method = RequestMethod.GET)
    @RequiresRoles(value={"admin"})
    public String test(){
        return "恭喜你,拿到了引數資訊";
    }
    
    /**
     * 3.2.當前介面需要經過角色與許可權的校驗(需包含admin的角色,以及角色的查詢 或 選單的查詢)
     * @return
     */
    @RequestMapping(value = "/info",method = RequestMethod.GET)
    @RequiresRoles(value={"admin"})
    @RequiresPermissions(value={"role:search","menu":"search"},logical=Logical.OR)
    public String test(){
        return "恭喜你,拿到了引數資訊";
    }
    
    
複製程式碼

在我們的實際使用過程中,實際上只需要使用@RequiresPermissions和@RequiresAuthentication就可以了這一個註解就可以了,在上一小節的結尾,我們採取了業務模組與操作的結合方案來解耦頁面和api介面的關係,和apache Shiro的這種方式正好一致。但是@RequiresRoles這個我們儘可能不採用,因為角色的組合形式太多,角色名沒有辦法在介面中具象唯一化(很難指定介面歸某個角色呼叫,但是一定能知道介面歸屬於某些業務模組的某些操作。)

現在我們來回顧一下整個運轉的流程。

shiro許可權的驗證流程
shiro許可權的驗證流程

如何編寫自定義註解

但是僅僅是擁有shiro中的這5個註解肯定是不夠使用的。在實際的使用過程中,根據需求,我們會在許可權認證中加入我們自己特有的業務邏輯的,我們為了便捷則可以採用自定義註解的方式進行使用。這種方法不僅僅適用於Apache Shiro,很多其他的框架如:Hibernate Validator、SpringMVC、甚至我們可以寫一套校驗體系,在aop中去驗證許可權,這都是沒問題的。所以自定義註解的作用很廣。但是在這裡,我僅僅基於shiro的來實現適用於它的自定義註解。

  • 定義註解類
/**
 * 用於認證的介面的註解,組合形式預設是“或”的關係
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
    /**
     * 業務模組
     * @return
     */
    String[] module();
    /**
     * 操作型別
     */
    String[] action();

}

複製程式碼
  • 定義註解的處理類
/**
 * Auth註解的操作類
 */
public class AuthHandler extends AuthorizingAnnotationHandler {


    public AuthHandler() {
        //寫入註解
        super(Auth.class);
    }

    @Override
    public void assertAuthorized(Annotation a) throws AuthorizationException {
        if (a instanceof Auth) {
            Auth annotation = (Auth) a;
            String[] module = annotation.module();
            String[] action = annotation.action();
            //1.獲取當前主題
            Subject subject = this.getSubject();
            //2.驗證是否包含當前介面的許可權有一個通過則通過
            boolean hasAtLeastOnePermission = false;
            for(String m:module){
                for(String ac:action){
                    //使用hutool的字串工具類
                    String permission = StrFormatter.format("{}:{}",m,ac);
                    if(subject.isPermitted(permission)){
                        hasAtLeastOnePermission=true;
                        break;
                    }
                }
            }
            if(!hasAtLeastOnePermission){
                throw new AuthorizationException("沒有訪問此介面的許可權");
            }

        }
    }
}
複製程式碼
  • 定義shiro攔截處理類
/**
 * 攔截器
 */
public class AuthMethodInterceptor extends AuthorizingAnnotationMethodInterceptor {


    public AuthMethodInterceptor() {
        super(new AuthHandler());
    }

    public AuthMethodInterceptor(AnnotationResolver resolver) {
        super(new AuthHandler(), resolver);
    }

    @Override
    public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {
        // 驗證許可權
        try {
            ((AuthHandler) this.getHandler()).assertAuthorized(getAnnotation(mi));
        } catch (AuthorizationException ae) {
            if (ae.getCause() == null) {
                ae.initCause(new AuthorizationException("當前的方法沒有通過鑑權: " + mi.getMethod()));
            }
            throw ae;
        }
    }
}

複製程式碼
  • 定義shiro的aop切面類
/**
 * shiro的aop切面
 */
public class AuthAopInterceptor extends AopAllianceAnnotationsAuthorizingMethodInterceptor {
    public AuthAopInterceptor() {
        super();
        // 新增自定義的註解攔截器
        this.methodInterceptors.add(new AuthMethodInterceptor(new SpringAnnotationResolver()));
    }
}

複製程式碼
  • 定義shiro的自定義註解啟動類
/**
 * 啟動自定義註解
 */
public class ShiroAdvisor extends AuthorizationAttributeSourceAdvisor {

    public ShiroAdvisor() {
        // 這裡可以新增多個
        setAdvice(new AuthAopInterceptor());
    }

    @SuppressWarnings({"unchecked"})
    @Override
    public boolean matches(Method method, Class targetClass) {
        Method m = method;
        if (targetClass != null) {
            try {
                m = targetClass.getMethod(m.getName(), m.getParameterTypes());
                return this.isFrameAnnotation(m);
            } catch (NoSuchMethodException ignored) {

            }
        }
        return super.matches(method, targetClass);
    }

    private boolean isFrameAnnotation(Method method) {
        return null != AnnotationUtils.findAnnotation(method, Auth.class);
    }
}

複製程式碼

總體的思路順序:定義註解類(定義業務可使用的變數)->定義註解處理類(通過註解中的變數做業務邏輯處理)->定義註解的攔截器->定義aop的切面類->最後定義shiro的自定義註解啟用類。其他的自定義的註解的編寫思路和這個也是類似的。

相關文章