如何優雅的使用切面和註解實現許可權驗證

一枚小白發表於2019-09-12

背景

許可權驗證在我們系統中是一個與業務邏輯無關但是又與業務息息相關的一個功能。
設想我們開發了一款為中小型企業定製的會員系統。這款系統可以為企業A、企業B等多種企業提供服務。資料庫中的表結構往往是這樣的(以下只是一個demo,實際情況中欄位一定會更多、更復雜):

id memberCardCode userName card_status business
1 a564456578 zhangsan 0 business-a
2 b678688643 lisi 1 businsss-b
3 a775445667 wangwu 0 businsss-a
4 b943578978 zhaoliu 1 businsss-b
5 c657688799 sunqi 1 businsss-c

基於上表,我們刪除id = 1的會員往往是這樣操作的(假設是物理刪除):
controller層:

@RestController("/member")
public class MemberController {
    @PostMapping("/delete")
    public void deleteById(int id) {
        // 此處省略刪除程式碼
    }
}
複製程式碼

最終在controller中呼叫的SQL語句是這樣的:

delete from member where id = 1;
複製程式碼

乍一看,就這樣一條簡單sql語句能有什麼問題呢?其實越是簡單的問題,越不能放過。
通過上表我們看到id = 1的會員資訊是屬於business-a的。所以理應是business-a的賬號才能刪除id = 1的會員資訊。那此時如果business-b在刪除會員的時候將引數id改為1,此時就會出現business-b刪除了business-a的會員。此時business-a的心情是崩潰的。

如何優雅的使用切面和註解實現許可權驗證
所以許可權驗證是非常必要。那許可權驗證怎麼做呢?

不太優雅:侵入業務程式碼的方案

在controller層加入邏輯判斷:判斷刪除的id是否屬於當前賬號,如果屬於則刪除;否則直接返回。程式碼如下:

@RestController("/member")
public class MemberController {
    @PostMapping("/delete")
    public void deleteById(int id) {
        // 第一步:許可權驗證
        Integer id = selectByIdAndBusiness(id, "business-a");
        if (id == null) {
            // 說明id不屬於business-a不能刪除
            return;
        }
        // 第二步:呼叫刪除邏輯
    }
}
複製程式碼

上面的程式碼確實解決了越權的問題,但是會將一些業務無關的程式碼侵入到我們的業務邏輯,這樣的實現邏輯不太優雅。那我們來選擇一個更優雅的方式來完成許可權驗證。

優雅:註解 + 切面的方案

首先我們需要明確關注的欄位資訊:id和business。其中business可以在filter中存入一個ThreadLocal,所以我們只需要關注欄位id即可。
第一步:建立自定義註解(作用於方法)

@Documented  
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.METHOD)  
public @interface Auth {
    // 方法的引數名稱,以防引數名稱不是id,所以提供paramName
    String paramName() default "id";
}
複製程式碼

第二步:在方法上使用註解

package com.demo.controller;
@RestController("/member")
public class MemberController {
    @PostMapping("/delete")
    @Auth(paramId = "deleteId")
    public void deleteById(int deleteId) {
        // 呼叫刪除邏輯
    }
}
複製程式碼

第三步:實現切面

// 通過註解可以看到,我們該方法切的是controller層帶有Auth註解的方法
@Before(value = "execution(public * com.demo.controller..*.*(..))"
      + " && @annotation(auth)", argNames = "pjp, auth")
public void before4Auth(JoinPoint pjp, Auth auth) {
    // 1、通過ThreadLocal獲取business
    String business = context.get();
    // 2、通過註解解析id
    // 2.1 獲取引數值
    Object[] args = pjp.getArgs();
    // 2.2 獲取引數名
    MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
    String[] parameterNames = methodSignature.getParameterNames();
    // 2.3 獲取註解中paramName的下標
    int index = ArrayUtils.indexOf(parameterNames, auth.paramName());
    // 2.4 根據下標獲取id對應的值
    int val = (int) args[index];
    // 2.5 鑑權邏輯
    Integer id = selectByIdAndBusiness(val, business);
    if (id == null) {
        // 拋異常提示越權
    }
    // 否則的正常執行下面的業務邏輯
}
複製程式碼

通過這種方式,我們只需在controller層的方法加上@Auth註解即可。沒有非業務程式碼的侵入,實現方式可算優雅。
如果您有更優雅的解決方案,歡迎提供思路。

相關文章