乐趣区

注解认证

问题描述
权限认证
权限认证一直是比较复杂的问题,如果是实验这种要求不严格的产品,直接逃避掉权限认证。
软件设计与编程实践的实验,后台直接用 Spring Data REST,好使是好使,但是不能在实际项目中运用,直接把 api 自动生成了,谁调用都行。
在商业项目中,没有权限是不行的。
注解
关于权限,一直没有找到很好的解决方案。直到网上送检项目,因功能简单,且用户角色单一,潘老师提出了利用注解实现权限认证的方案。

两个注解,AdminOnly 标注只能给管理员用的方法,Anonymous 标注对外的无需认证的接口,其他的未标注的是给普通用户使用的。
示例代码
示例代码地址:todo
开发环境:Java 1.8 + Spring Boot 2.1.2.RELEASE
实现
拦截器
根据三类方法,对用户权限进行拦截,使用拦截器 + AOP 的模式实现。

拦截器拦截下那些没有 AdminOnly 与 Anonymous 注解标注的方法请求,并进行用户认证。
拦截器过完之后,去执行请求方法。
AOP 切 AdminOnly 注解的前置通知,植入一段管理员认证的切面逻辑。
对 Anonymous 注解不进行任何处理,实现了匿名用户的访问。
区别
这样一看,拦截器就和 AOP 很像。那是因为我们这个例子还远没有发挥出 AOP 的实际价值。
AOP 比这个例子中看上去,强大得多。
最近学习了设计模式中的代理模式,与 AOP 息息相关,我会在以后的文章中与大家一同学习。
拦截器
声明拦截器,第三个参数就是当前被拦截的方法。
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
}
基本思路
利用反射获取当前方法中是否标注有 AdminOnly 与 Anonymous 注解,如果没有,则进行普通用户认证。

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 中使用 Google 的 Gson 进行数据解析,这个在我们后台使用的还是挺广泛的,和阿里的 fastjson 齐名,都是非常火的 json 库。
NYTimes 的工程师发现 Gson 中使用反射来获取数据类型,导致应用启动时增加了大约 700ms 的延迟。
ActiveAndroid 是一个使用反射实现的库,特意去 Github 逛了一手,4000 多 star,这是相当流行的开源项目了!

Scribd:1093ms for call com.activeandroid.ActiveAndroid.initialize。
Myntra:1421ms 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);
以后如果认证注解多了呢?
我们期待这样,有一个通用的注解来判定当前方法是否要被拦截,而 AdminOnly 和 Anonymous 应继承该注解的功能,这样以后再想添加不被拦截器拦截的注解,就不需要修改启动时扫描的方法了。
// 获取授权注解
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 异常 ”);
}
}
}
}
总结
学会了一个解决问题的新办法:某个框架应该也遇到过你所遇到的问题,去找找框架中的工具类,这可能会很有帮助。

退出移动版