资讯专栏INFORMATION COLUMN

注解认证

fancyLuo / 1532人阅读

摘要:拦截器拦截下那些没有与注解标注的方法请求,并进行用户认证。直接根据编写的代码生成原生的代码,所以不会存在任何性能问题解决方案为了解决拦截器中使用反射的性能问题,我们学习的设计思路,在启动时直接完成所有反射注解的读取,存入内存。

问题描述 权限认证

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

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

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

注解

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

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

示例代码

示例代码地址:auth-annotation - mengyunzhi

开发环境: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 getMethodAnnotation(Class annotationType) {
    return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType);
}

该方法又调用了AnnotatedElementUtils.findMergedAnnotation方法,我们再点进去看看:

org.springframework.core.annotation.AnnotatedElementUtils中的findMergedAnnotation实现:

@Nullable
public static  A findMergedAnnotation(AnnotatedElement element, Class 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 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 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 {

    // 基础包名
    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 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异常");
            }
        }
    }
}
总结

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73201.html

相关文章

  • 基于spring-security-oauth2实现单点登录(持续更新)

    摘要:认证服务器和浏览器控制台也没有报错信息。这里简单介绍下如何查阅源码,首先全局搜索自己的配置因为这个地址是认证服务器请求授权的,所以,请求认证的过滤器肯定包含他。未完待续,下一篇介绍资源服务器和认证服务器的集成。 基于spring-security-oauth2-实现单点登录 文章代码地址:链接描述可以下载直接运行,基于springboot2.1.5,springcloud Green...

    妤锋シ 评论0 收藏0
  • Shiro【授权过滤器、与ehcache整合、验证码、记住我】

    摘要:为了达到很好的效果,我们使用来对的缓存进行管理配置会话管理器,对会话时间进行控制手动清空缓存由于验证用户名和密码之前,一般需要验证验证码的。 前言 本文主要讲解的知识点有以下: Shiro授权过滤器使用 Shiro缓存 与Ehcache整合 Shiro应用->实现验证码功能 记住我功能 一、授权过滤器测试 我们的授权过滤器使用的是permissionsAuthorization...

    K_B_Z 评论0 收藏0
  • 基于shiro的自定义注解的扩展

    摘要:的自身注解的用法。所以自定义注解的作用很广。但是在这里,我仅仅基于的来实现适用于它的自定义注解。其他的自定义的注解的编写思路和这个也是类似的。 基于shiro的自定义注解的扩展 根据我的上一篇文章,权限设计的杂谈中,涉及到了有关于前后端分离中,页面和api接口断开表与表层面的关联,另辟蹊径从其他角度找到方式进行关联。这里我们主要采取了shiro的自定义注解的方案。本篇文章主要解决以下的...

    YuboonaZhang 评论0 收藏0

发表评论

0条评论

fancyLuo

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<