基于注解的接口限流+统一session认证
代码心得:
一个基本的做法:对于用户身份认证做到拦截器里,针对HandlerMethod进行统一拦截认证,根据方法上的注解标识,判别是否需要身份验证,并将查找出来的User实体存入ThreadLocal,在本次请求中,以后的目标方法都可以从ThreadLocal里取出user,并在afterCompletion方法里,对ThreadLocal做清理,也可以通过HandlerMethodArgumentResolver针对某一种参数类型(UserParameter.class)编写统一的用户认证,当然,也可以先拦截,然后在ArgumentResolver里通过ThreadLocal获取user注入到controller方法里的参数
tip:一个请求(即一个thread),通过拦截器将DB中的对象存入ThreadLocal,之后目标方法,或本次请求中的其他后续方法,都可以通过ThreadLocal获取该对象,然后在afterCompletion清理ThreadLocal,表示一个请求结束。当然,拦截器里可以在redis,如果session失效,可以重定向到登陆。
拦截器主要用来拦截handlerMethod(controller方法),aop用于service层面(也可以用于某些工具类,或者controller)的非业务统一处理,前置后置处理器用于全部bean的初始化前或初始化后的统一代码织入
基于注解的接口限流+统一session认证代码实现:
第一步:编写AccessLimit注解
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface AccessLimit {//控制单位时间内的最大访问次数 int maxCount();//注解特点:标记属性即调用方法 int seconds(); boolean needLogin() default true;//是否需要用户身份 }
第二步:编写拦截器
@Component 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);// 请求(一个thread)->拦截器->AOP->目标方法->清理user HandlerMethod hm = (HandlerMethod)handler;//对controller里的方法做了封装,避免运行时使用反射 AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class); if(accessLimit == null) { 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);//可以重定向到登陆或向浏览器输出json return false;//返回false拦截,返回true放行 } key += "_" + user.getId();// 接口防刷:需要用户登陆的 接口路径+用户id做key 不要的:接口路径做key }else { //do nothing }
//redis限速器 AccessKey ak = AccessKey.withExpire(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; } //向浏览器写出json 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.write(byte[]) out.flush(); out.close(); } //从请求中获取用户信息 private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) {
//从request或cookie中获取token,防止不支持cookie的client 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);// service里也是返回null,最上级在根据null处理异常 } 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; }
将编写好的拦截器加入webConfig(即继承webConfigAdapter)中,即可
使用:
@AccessLimit(seconds=5, maxCount=5, needLogin=true)
@RequestMapping(value="/path", method=RequestMethod.GET)
@ResponseBody
public Result<String> getMiaoshaPath(HttpServletRequest request, MiaoshaUser user){...}