阿昌教你自定义拦截器&自定义参数解析器&自定义包装HttpServletRequest
阿昌 Java小菜鸡
## 前言

这次也是依然在学习开源项目Tduck-填鸭收集器的时,阿昌在研究这项目是如何进行安全校验的,我一开始在项目里面查Shiro/SpringSecurity,我以为他使用了市面主流的安全框架,但是发现,他根本没有使用,而是自定义了一系列的 拦截器&过滤器 来实现安全的校验。

比如,通过自定义注解来决定这个资源是否需要用户登录才能够访问。

在项目中我发现的自定义注解有三个

  • @Login
  • @LoginUser
  • @NoRepeatSubmit

在开始前,放上SpringMVC的执行流程 镇场: (●′ω`●)!!!

image

•́ . •̀) 下面是简单的记录在项目中,学习到的内容,且对于这个项目的这3个注解的理解!!!*


@Login

com.tduck.cloud.api.annotation.Login

我先看的是@Login这个注解,通过这个注解去控制这个controller资源是需要在需要登录才能调用的接口使用

给他定义的是:描述方法/运行时生效

1
2
3
4
5
6
7
8
/**
* 登录验证 在需要登录才能调用的接口使用
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Login {
}

我在想他是如何生效的呢???

很容易让我想到AOP的实现,于是我就去翻我 AOP的笔记

都知道AOP需要3个要点 【 1. 额外功能 2. 切⼊点 3. 组装切⾯ 】,但是我在这个项目里面找,发现并没有找到,所以就排除了他这个注解是通过AOP实现的方式。

那他是通过什么去给这个注解写入对应的判断逻辑呢??? 我找了一圈,发现

com.tduck.cloud.api.web.interceptor.AuthorizationInterceptor这里有一个拦截器

image

他继承了HandlerInterceptorAdapter,重写了preHandle()方法,那这里我就纳闷了,HandlerInterceptorAdapter也是 拦截器吗???于是我就按下Ctrl+H,看一下类关系图。

这里他是通过适配器设计模式实现,

image

他们这个AuthorizationInterceptor就是HandlerInterceptor接口下的一个实现类,所以就可以重写HandlerInterceptor的一系列方法。

那到了这里,还是没说出来他是如何去根据@Login注解去判断对应的逻辑,再往下看。

在AuthorizationInterceptor中他重写了preHandle方法,

他在方法中判断是否是HandlerMethod,如果是就强转,并通过getMethodAnnotation(Login.class),去获取到他是否有Login.class,那个Login.class是什么???

看了下方法名,你会马上看出来,这个Login.class就是上面说的@Login注解

image

那这里有一个问题,HandlerMethod是什么东西?他为什么会能有getMethodAnnotation()方法去拿到给他传入的.class的注解呢?

那这里就要引出SpringMVC了,如果你学了它,你就会知道HandlerMethod,其实就对应Handler,也就是我们写的那些Controller

image

那知道了这个之后,根据上面的handler instanceof HandlerMethod来判断他是不是controller,如果是,那就可以拿到controller上面所标记的注解,

image

以上就可以看到了@Login所进行的流程,最后写了以上的还是不行的,我们需要将这个拦截器加入到spring的拦截器链表中也就是上面的图,让他起作用

image

查找了下代码,他在com.tduck.cloud.api.config.WebMvcConfig中实现了WebMvcConfigurer

重写了addInterceptors(InterceptorRegistry registry)方法,把上面写的登录校验加入拦截器链中,并指定拦截所有请求!

image

这里的最后,查看他随便使用到@Login注解的地方,所以标注了它后会在拦截器中进行对token的校验,来判断用户是否登录

image


@LoginUser

com.tduck.cloud.api.annotation.LoginUser

通过这个注解去控制这个某个参数封装用户信息

给他定义的是:描述参数/运行时生效

1
2
3
4
5
6
/**
* 登录用户信息
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {}

在项目中查找@LoginUser注解,发现在com.tduck.cloud.api.web.resolver.LoginUserHandlerMethodArgumentResolver中,他实现了HandlerMethodArgumentResolver参数解析器,他就是解析请求发来的参数并解析成对应我们controller中对应方法参数,比如@RequestBody、@RequestParam等

HandlerMethodArgumentResolver的小文章 •‾̑⌣‾̑•) 】

image

需要获取到请求来的参数,并封装。

但是这里需要自定义,因为SpringMVC只封装对应的请求体,这里就直接自定义写了一个封装器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

@Component
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
private final UserService userService;

//通过构造依赖,需要的userService
public LoginUserHandlerMethodArgumentResolver(UserService userService) {
this.userService = userService;
}

//这个方法返回的T/F,会直接决定下面的resolveArgument()方法是否能够被执行
@Override
public boolean supportsParameter(MethodParameter parameter) {
//拿到这个请求参数的类型
//判断他是不是UserEntity类,并判断他是不是有被注解@LoginUser标注
return parameter.getParameterType().isAssignableFrom(UserEntity.class) && parameter.hasParameterAnnotation(LoginUser.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
NativeWebRequest request, WebDataBinderFactory factory) throws Exception {
//从请求域中拿到用户的id
//这个用户的id是在 【@Login的拦截器里面最后放入到请求域中的】
Object object = request.getAttribute(AuthorizationInterceptor.USER_KEY, RequestAttributes.SCOPE_REQUEST);
if (object == null) {
return null;
}

//获取用户信息,并返回,返回后spring会自动给UserEntity类封装上查询到的信息
UserEntity user = userService.getById((Long) object);
return user;
}
}

最后写完了肯定要加入到MVCArgumentResolvers参数解析器s中,

com.tduck.cloud.api.config.WebMvcConfig

image

这样子,主要标注了@LoginUser的注解就会被这个参数解析器解析,并自动去查询获取封装对应的用户数据

同样这里的最后,也查看羡慕使用到@LoginUser注解的地方,发现他使用的方式跟我们常用的各种封装数据类型的注解一模一样

com.tduck.cloud.api.web.controller.UserController

image


@NoRepeatSubmit

com.tduck.cloud.api.annotation.NoRepeatSubmit

最后一个注解,通过它来控制不允许重复提交

给他定义的是:描述方法、描述类&接口/运行时生效

1
2
3
4
5
6
/**
* 不允许重复提交注解
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {}

如果你观察仔细,你在上面就会已经发现了这个注解的拦截器跟@Login的拦截器在同一个包下

com.tduck.cloud.api.web.interceptor.NoRepeatSubmitInterceptor

不同的是,他是直接实现的HandlerInterceptor,而@Login的拦截器是继承HandlerInterceptor的一个实现类HandlerInterceptorAdapter

HandlerInterceptor&HandlerInterceptorAdapter区别 】 ´•.̫ • `


它先判断是否是HandlerMethod,就是判断是不是Handler处理器,如果不是,就直接结束返回true;

强转,并再判断他是否又被@NoRepeatSubmit标注,如果有就继续,不然就直接结束返回true;

在这里看到,他通过自定义的方式跟@Login是一样的 (ฅ´ω`ฅ)!!!

image

然后判断是否是BodyReaderHttpServletRequestWrapper

image

BodyReaderHttpServletRequestWrapper是什么东西???

关联打开后发现代码很多!!! 且发现他继承了HttpServletRequestWrapper

image

那问题就转移了,就是HttpServletRequestWrapper是什么???

HttpServletRequestWrapper类的作用

这里简单的概括就是因为我们很多时候都要改变HttpServletRequest,但是由于java.util.Map包装的HttpServletRequest对象的参数是不可改变,所以通过 【装饰者设计模式 】包装来改变其状态,这样子只需要在装饰类HttpServletRequestWrapper中,按照需要重写其对应的方法即可

com.tduck.cloud.api.web.wrapper.BodyReaderHttpServletRequestWrapper

进行了对原HttpServletRequest包装装饰,给每一个参数内容都加上了XSS过滤

image

回到之前上面,通过继承HttpServletRequestWrapper类来,包装进行XSS过滤,请求Request,

那是什么时机进行对其XSS过滤呢???

在全项目搜索BodyReaderHttpServletRequestWrapper,发现在SignAuthFilter进行了包装过滤XSS

com.tduck.cloud.api.web.filter.SignAuthFilter

image

再一次回来,然后获取对应的数据,判断redis中是否存在,有就拦截,没有就给redis设置过期时间为2s避免了重复提交

image

同样,他肯定也需要加入到MVC的拦截器链中,com.tduck.cloud.api.config.WebMvcConfig

image

在最后,还是依然的查看他使用到@NoRepeatSubmit注解的地方,

com.tduck.cloud.api.web.controller.UserProjectResultController

这里是业务逻辑是填写表单等,通过@NoRepeatSubmit,来避免重复提交

image


总结

以上的内容就全部记录完毕了,感谢你能认认真真看完,一定会有大量的收获!!!

这里涉及到了大量的MVC知识点 •̀∀•́ )!!!

  • 适配器设计模式
  • MVC如何新增自定义拦截器
  • HandlerMethodArgumentResolver 自定义参数解析器
  • HandlerInterceptorAdapter 和 HandlerInterceptor区别
  • 装饰者设计模式
  • HttpServletRequestWrapper 自定义包装HttpServletRequest
 请作者喝咖啡