註解認證

問題描述

權限認證

權限認證一直是比較複雜的問題,若是是實驗這種要求不嚴格的產品,直接逃避掉權限認證。html

軟件設計與編程實踐的實驗,後臺直接用Spring Data REST,好使是好使,可是不能在實際項目中運用,直接把api自動生成了,誰調用都行。java

在商業項目中,沒有權限是不行的。android

註解

關於權限,一直沒有找到很好的解決方案。直到網上送檢項目,因功能簡單,且用戶角色單一,潘老師提出了利用註解實現權限認證的方案。git

clipboard.png

兩個註解,AdminOnly標註只能給管理員用的方法,Anonymous標註對外的無需認證的接口,其餘的未標註的是給普通用戶使用的。github

示例代碼

示例代碼地址:auth-annotation - mengyunzhiweb

開發環境:Java 1.8 + Spring Boot 2.1.2.RELEASEspring

實現

攔截器

根據三類方法,對用戶權限進行攔截,使用攔截器 + AOP的模式實現。編程

clipboard.png

攔截器攔截下那些沒有AdminOnlyAnonymous註解標註的方法請求,並進行用戶認證。json

攔截器過完以後,去執行請求方法。設計模式

AOPAdminOnly註解的前置通知,植入一段管理員認證的切面邏輯。

Anonymous註解不進行任何處理,實現了匿名用戶的訪問。

區別

這樣一看,攔截器就和AOP很像。那是由於咱們這個例子還遠沒有發揮出AOP的實際價值。

AOP比這個例子中看上去,強大得多。

最近學習了設計模式中的代理模式,與AOP息息相關,我會在之後的文章中與你們一同窗習。

攔截器

聲明攔截器,第三個參數就是當前被攔截的方法。

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    HandlerMethod handlerMethod = (HandlerMethod) handler;
}

基本思路

利用反射獲取當前方法中是否標註有AdminOnlyAnonymous註解,若是沒有,則進行普通用戶認證。

clipboard.png

AdminOnly adminOnly = handlerMethod.getMethodAnnotation(AdminOnly.class);
Anonymous anonymous = handlerMethod.getMethodAnnotation(Anonymous.class);

if (adminOnly != null && anonymous != null) {
    return true;
}

boolean result = false;

// 進行用戶認證

return result;

性能優化

反射

每次請求,都要走攔截器,調用getMethodAnnotation方法。

咱們去看看getMethodAnnotation方法的源碼實現:

org.springframework.web.method.HandlerMethod中的getMethodAnnotation方法:

@Nullable
public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
    return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType);
}

該方法又調用了AnnotatedElementUtils.findMergedAnnotation方法,咱們再點進去看看:

org.springframework.core.annotation.AnnotatedElementUtils中的findMergedAnnotation實現:

@Nullable
public static <A extends Annotation> A findMergedAnnotation(AnnotatedElement element, Class<A> annotationType) {
    // Shortcut: directly present on the element, with no merging needed?
    A annotation = element.getDeclaredAnnotation(annotationType);
    if (annotation != null) {
        return AnnotationUtils.synthesizeAnnotation(annotation, element);
    }

    // Exhaustive retrieval of merged annotation attributes...
    AnnotationAttributes attributes = findMergedAnnotationAttributes(element, annotationType, false, false);
    return (attributes != null ? AnnotationUtils.synthesizeAnnotation(attributes, annotationType, element) : null);
}

該方法是調用AnnotatedElement接口中聲明的getDeclaredAnnotation方法進行註解獲取:

AnnotatedElement接口,存在於java反射包中:

clipboard.png

話很少說,反射,就存在性能問題!

我的理解

一樣是Java,咱們看看Google對於Android反射的態度就行了。

我記得以前我去過Google Android的官網,官方不推薦在Android中使用框架,這可能帶來嚴重的性能問題,其中就有考慮到傳統Java框架中大量使用的反射。

這是國外一篇關於反射的文章,反射到底有多慢?:How Slow is Reflection in Android?

文中提到了一項規範,即用戶期待應用的啓動時間的平均值爲2s

NYTimes Android App中使用GoogleGson進行數據解析,這個在咱們後臺使用的仍是挺普遍的,和阿里的fastjson齊名,都是很是火的json庫。

NYTimes的工程師發現Gson中使用反射來獲取數據類型,致使應用啓動時增長了大約700ms的延遲。

ActiveAndroid是一個使用反射實現的庫,特地去Github逛了一手,4000star,這是至關流行的開源項目了!

clipboard.png

Scribd1093ms for call com.activeandroid.ActiveAndroid.initialize

Myntra1421ms for call com.activeandroid.ActiveAndroid.initialize

Data-Binding

打臉?Android不是不推薦使用框架嗎?那爲何Google又推出了Data-Binding呢?

注意,Google考慮的是第三方框架高額的開銷而引起性能問題。

去看看Data-Binding的優勢,最重要的一條就是該框架不使用反射,使用動態代碼生成技術,不會由於使用該框架而形成性能問題。

直接根據編寫的代碼生成原生Android的代碼,因此不會存在任何性能問題!

解決方案

爲了解決攔截器中使用反射的性能問題,咱們學習SpringBoot的設計思路,在啓動時直接完成全部反射註解的讀取,存入內存。

以後每次攔截器直接從內存中讀取,提升性能。

監聽容器啓動事件,在容器啓動時執行如下代碼,掃描全部控制器,及其方法上的註解,若是符合條件,則放到HashMap中。

// 初始化組件掃描Scanner,禁用默認的filter
ClassPathScanningCandidateComponentProvider scanner =
        new ClassPathScanningCandidateComponentProvider(false);
// 添加過濾條件,要求組件上有RestController註解
scanner.addIncludeFilter(new AnnotationTypeFilter(RestController.class));
// 在當前項目包下掃描全部符合條件的組件
for (BeanDefinition beanDefinition : scanner.findCandidateComponents(basePackageName)) {
    // 獲取當前組件的完整類名
    String name = beanDefinition.getBeanClassName();
    try {
        // 利用反射獲取相關類
        Class<?> clazz = Class.forName(name);
        // 初始化方法名List
        List<String> methodNameList = new ArrayList<>();
        // 獲取當前類(不包括父類,因此要求控制器間不能繼承)中全部聲明方法
        for (Method method : clazz.getDeclaredMethods()) {
            // 獲取方法上的註解
            AdminOnly adminOnly = method.getAnnotation(AdminOnly.class);
            Anonymous anonymous = method.getAnnotation(Anonymous.class);
            // 若是該方法不存在AdminOnly和Anonymous註解
            if (adminOnly == null && anonymous == null) {
                // 添加到List中
                methodNameList.add(method.getName());
            }
        }
        // 添加到Map中
        AuthAnnotationConfig.getAnnotationsMap().put(clazz, methodNameList);
    } catch (ClassNotFoundException e) {
        logger.error("掃描註解配置時,發生了ClassNotFoundException異常");
    }
}

攔截器修改

原來的攔截器是這樣的:

AdminOnly adminOnly = handlerMethod.getMethodAnnotation(AdminOnly.class);
Anonymous anonymous = handlerMethod.getMethodAnnotation(Anonymous.class);

if (adminOnly != null && anonymous != null) {
    return true;
}

boolean result = false;

// 進行用戶認證

return result;

如今是這樣的:

logger.debug("獲取當前請求方法的組件類型");
Class<?> clazz = handlerMethod.getBeanType();

logger.debug("獲取當前處理請求的方法名");
String methodName = handlerMethod.getMethod().getName();

logger.debug("獲取當前類中需認證的方法名");
List<String> authMethodNames = AuthAnnotationConfig.getAnnotationsMap().get(clazz);

logger.debug("若是List爲空或者不包含在認證方法中,釋放攔截");
if (authMethodNames == null || !authMethodNames.contains(methodName)) {
    return true;
}

logger.debug("進行用戶認證");
boolean result = false;

// 用戶認證

return result;

以前用了兩次反射,如今是調用了handlerMethod.getBeanType()handlerMethod.getMethod().getName()

再去看看這兩個的實現:

getBeanType

public Class<?> getBeanType() {
    return this.beanType;
}

getMethod

public Method getMethod() {
    return this.method;
}

都是在org.springframework.web.method.HandlerMethod類中直接返回屬性,咱們推斷:這個HandlerMethod,應該是Spring在容器啓動時就已經構造好的方法對象,在攔截器執行期間,沒有調用反射。

註解的註解

如今是註解少,咱們寫兩行,感受問題不大:

// 獲取方法上的註解
AdminOnly adminOnly = method.getAnnotation(AdminOnly.class);
Anonymous anonymous = method.getAnnotation(Anonymous.class);

之後若是認證註解多了呢?

咱們期待這樣,有一個通用的註解來斷定當前方法是否要被攔截,而AdminOnlyAnonymous應繼承該註解的功能,這樣之後再想添加不被攔截器攔截的註解,就不須要修改啓動時掃描的方法了。

// 獲取受權註解
AdminAuth adminAuth = method.getAnnotation(AdminAuth.class);

咱們指望像Spring Boot同樣,在註解上加註解,實現複合註解。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
}

構造註解

若是對Java自定義註解不瞭解,能夠去慕課網學習相關課程:全面解析Java註解 - 慕課網

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AdminAuth {
}

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}),該註解能夠標註在方法上,也能夠標註在其餘註解上。

@Retention(RetentionPolicy.RUNTIME),該註解一直保留到程序運行期間。

給註解加註解

AdminOnly:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@AdminAuth
public @interface AdminOnly {
}

Anonymous:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@AdminAuth
public @interface Anonymous {
}

解析註解

加註解很簡單,重要的是怎麼解析該註解。

調用反射包中的Method類提供的getAnnotation方法,只會告訴咱們當前標註了什麼註解。

好比:

@AdminOnly
public void test() {
}

咱們能夠經過getAnnotation獲取AdminOnly,可是獲取不到註解在@AdminOnly上的@AdminAuth註解。

怎麼獲取註解的註解呢?

找了一上午,不得不說,我解決這個問題仍是靠必定的運氣的。在我要放棄的時候,在Google搜出了SpringFramework中的註解工具類AnnotationUtils

隨手打開文檔:Class AnnotationUtils - Spring Core Docs

第四個方法就是我想要的:

clipboard.png

使用該工具類,能直接獲取方法上標註在註解上的註解:

@AdminOnly
public void test() {
}
AdminAuth adminAuth = AnnotationUtils.getAnnotation(method, AdminAuth.class);

這種方法能獲取到標註在test方法上繼承而來的@AdminAuth註解。

最終代碼:

@Component
public class InitAnnotationsConfig implements ApplicationListener<ContextRefreshedEvent> {

    // 基礎包名
    private static final String basePackageName = "com.mengyunzhi.checkApplyOnline";
    // 日誌
    private static final Logger logger = LoggerFactory.getLogger(InitAnnotationsConfig.class);

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 初始化組件掃描Scanner,禁用默認的filter
        ClassPathScanningCandidateComponentProvider scanner =
                new ClassPathScanningCandidateComponentProvider(false);
        // 添加過濾條件,要求組件上有RestController註解
        scanner.addIncludeFilter(new AnnotationTypeFilter(RestController.class));
        // 在當前項目包下掃描全部符合條件的組件
        for (BeanDefinition beanDefinition : scanner.findCandidateComponents(basePackageName)) {
            // 獲取當前組件的完整類名
            String name = beanDefinition.getBeanClassName();
            try {
                // 利用反射獲取相關類
                Class<?> clazz = Class.forName(name);
                // 初始化方法名List
                List<String> methodNameList = new ArrayList<>();
                // 獲取當前類(不包括父類,因此要求控制器間不能繼承)中全部聲明方法
                for (Method method : clazz.getDeclaredMethods()) {
                    // 獲取受權註解
                    AdminAuth adminAuth = AnnotationUtils.getAnnotation(method, AdminAuth.class);
                    // 若是該方法不被受權,則須要認證
                    if (adminAuth == null) {
                        // 添加到List中
                        methodNameList.add(method.getName());
                    }
                }
                // 添加到Map中
                AuthAnnotationConfig.getAnnotationsMap().put(clazz, methodNameList);
            } catch (ClassNotFoundException e) {
                logger.error("掃描註解配置時,發生了ClassNotFoundException異常");
            }
        }
    }
}

總結

學會了一個解決問題的新辦法:某個框架應該也遇到過你所遇到的問題,去找找框架中的工具類,這可能會頗有幫助。

相關文章
相關標籤/搜索