注解认证

栏目: Java · 发布时间: 5年前

内容简介:权限认证一直是比较复杂的问题,如果是实验这种要求不严格的产品,直接逃避掉权限认证。软件设计与编程实践的实验,后台直接用在商业项目中,没有权限是不行的。

问题描述

权限认证

权限认证一直是比较复杂的问题,如果是实验这种要求不严格的产品,直接逃避掉权限认证。

软件设计与编程实践的实验,后台直接用 Spring Data REST ,好使是好使,但是不能在实际项目中运用,直接把 api 自动生成了,谁调用都行。

在商业项目中,没有权限是不行的。

注解

关于权限,一直没有找到很好的解决方案。直到网上送检项目,因功能简单,且用户角色单一,潘老师提出了利用注解实现权限认证的方案。

注解认证

两个注解, AdminOnly 标注只能给管理员用的方法, Anonymous 标注对外的无需认证的接口,其他的未标注的是给普通用户使用的。

示例代码

示例代码地址:todo

开发环境: Java 1.8 + Spring Boot 2.1.2.RELEASE

实现

拦截器

根据三类方法,对用户权限进行拦截,使用拦截器 + AOP 的模式实现。

注解认证

拦截器拦截下那些没有 AdminOnlyAnonymous 注解标注的方法请求,并进行用户认证。

拦截器过完之后,去执行请求方法。

AOPAdminOnly 注解的前置通知,植入一段管理员认证的切面逻辑。

Anonymous 注解不进行任何处理,实现了匿名用户的访问。

区别

这样一看,拦截器就和 AOP 很像。那是因为我们这个例子还远没有发挥出 AOP 的实际价值。

AOP 比这个例子中看上去,强大得多。

最近学习了 设计模式 中的代理模式,与 AOP 息息相关,我会在以后的文章中与大家一同学习。

拦截器

声明拦截器,第三个参数就是当前被拦截的方法。

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

基本思路

利用反射获取当前方法中是否标注有 AdminOnlyAnonymous 注解,如果没有,则进行普通用户认证。

注解认证

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 反射包中:

注解认证

话不多说,反射,就存在性能问题!

个人理解

同样是 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 ,这是相当流行的开源项目了!

注解认证

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

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

注解认证

使用该工具类,能直接获取方法上标注在注解上的注解:

@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异常");
            }
        }
    }
}

总结

学会了一个解决问题的新办法:某个框架应该也遇到过你所遇到的问题,去找找框架中的工具类,这可能会很有帮助。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

网站重构(第3版)

网站重构(第3版)

[美] Jeffrey Zeldman、[美] Ethan Marcotte / 傅捷、祝军、李宏 / 电子工业出版社 / 2011-3 / 59.00元

《网站重构:应用Web标准进行设计(第3版)》内容简介:畅销书作家、设计师、网页标准教父jeffrey zeldman再次更新了他经典的、颠覆行业的指南书。这已经是《网站重构:应用Web标准进行设计(第3版)》的第3版了,此次更新基本涵盖了随着环境和技术的变化,web标准所面临的挑战以及因此而发生的改善。第3版让基于标准的设计思想更加清晰,更加易于理解,帮助你在这个领域中保持聪明和领先。 ......一起来看看 《网站重构(第3版)》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

随机密码生成器
随机密码生成器

多种字符组合密码

html转js在线工具
html转js在线工具

html转js在线工具