前置拦截器的使用
在项目开发中,想要实现一个防刷的功能,主要是通过在redis中缓存URL(path+userId)被某个用户访问次数的标记,当在规定时间内访问次数过多则会进行拒绝访问,防止恶意刷单破坏公平性,也过滤了一些访问底层数据库的无效操作。
初步实现:
刚开始是在控制层对应接口方法上加一层验证代码,这样导致了控制层代码显得很臃肿,而且如果其他方法也需要的相同的验证,还要再写一段类似的代码,没有实现代码复用。
优雅实现:
使用注解来实现
我的另一篇博客简单介绍了自定义注解的实现,这次我是用拦截器来实现注解的功能。同时我将参数解析器的实现一并放到拦截器中,然后导入到ThreadLocal
中以实现私有。(参数解析实现文章链接)
拦截器:我个人觉得就是面向切面编程的一种体现,我这里使用的是前置拦截。
- 首先先建立一个注解
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Retention(RUNTIME)
@Target(METHOD)
public @interface AccessLimit {
int seconds();
int maxCount();
boolean needLogin() default true;
}
- 拦截器的实现
1、继承HandlerInterceptorAdapter
实现preHandle
方法
2、注册到WebMvcConfigurerAdapter
的InterceptorRegistry
中
3、将参数解析器实现整合到拦截器中
@Service
public class AccessInterceptor extends HandlerInterceptorAdapter{
@Autowired
MiaoshaUserService userService;
@Autowired
RedisService redisService;
/*
* 前置拦截器
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if(handler instanceof HandlerMethod) {//判断是否为控制层方法
MiaoshaUser user = getUser(request, response);
UserContext.setUser(user);//导入到当前线程中
HandlerMethod hm = (HandlerMethod)handler;
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if(accessLimit == null) {//是否有@AccessLimit注解
return true;
}
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key = request.getRequestURI();
if(needLogin) {
if(user == null) {
render(response, CodeMsg.SESSION_ERROR);
return false;
}
key += "_" + user.getId();
}else {
//do nothing
}
AccessKey ak = AccessKey.withExpire(seconds);//seconds秒内更新缓存
Integer count = redisService.get(ak, key, Integer.class);
if(count == null) {
redisService.set(ak, key, 1);
}else if(count < maxCount) {
redisService.incr(ak, key);
}else {
render(response, CodeMsg.ACCESS_LIMIT_REACHED);
return false;
}
}
return true;
}
//发送给前端错误信息
private void render(HttpServletResponse response, CodeMsg cm)throws Exception {
response.setContentType("application/json;charset=UTF-8");//防乱码
OutputStream out = response.getOutputStream();
String str = JSON.toJSONString(Result.error(cm));
out.write(str.getBytes("UTF-8"));
out.flush();
out.close();
}
//获得用户信息
private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) {
String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return null;
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
return userService.getByToken(response, token);
}
private String getCookieValue(HttpServletRequest request, String cookiName) {
Cookie[] cookies = request.getCookies();
if(cookies == null || cookies.length <= 0){
return null;
}
for(Cookie cookie : cookies) {
if(cookie.getName().equals(cookiName)) {
return cookie.getValue();
}
}
return null;
}
}
- 注册到web配置适配器中
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
UserArgumentResolver userArgumentResolver;
@Autowired
AccessInterceptor accessInterceptor;
/*
*注册参数解析器
*/
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
/*
*注册拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessInterceptor);
}
}
- 用户对象信息共享类
/**
* 获得当前线程的user信息
*通过ThreadLocal实现线程公用
*/
public class UserContext {
private static ThreadLocal<MiaoshaUser> userHolder = new ThreadLocal<MiaoshaUser>();
public static void setUser(MiaoshaUser user) {
userHolder.set(user);
}
public static MiaoshaUser getUser() {
return userHolder.get();
}
}
- 参数解析器的实现(将前端信息转化为控制层所需的对象)
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
MiaoshaUserService userService;
public boolean supportsParameter(MethodParameter parameter) {
Class<?> clazz = parameter.getParameterType();
return clazz==MiaoshaUser.class;
}
/*
* 获取当前线程的user
*/
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return UserContext.getUser();
}
}
总结:
实践了一个前置拦截器的简单实现,同时还使用了ThreadLocal
以实现线程中对象的共用。
拦截器和注解的设计,使代码更加简洁优雅,实现业务代码和功能代码的解耦,提高了代码的复用性和整个代码架构的结构性,对后续开发和他人了解项目都很有帮助。
- HandlerInterceptorAdapter的基本介绍和使用:拦截器的实现可以继承这个抽象类
- HandlerMethod介绍与使用:可以看作是表示控制层的方法,也可以使用这个类中的bean属性确定到一个确定控制层方法的名称
SpringMVC中处理请求的方法叫做HandlerMethod,HandlerMethod可以通过多种方式声明它的参数来源,同时对应着不同的解析过程。
public HandlerMethod createWithResolvedBean() {
Object handler = this.bean;
if (this.bean instanceof String) {
String beanName = (String) this.bean;//获得控制层方法名称
handler = this.beanFactory.getBean(beanName);
}
return new HandlerMethod(this, handler);
}