接口限流防刷:
限制同一个用户在限定时间内,只能访问固定次数。
思路:每次点击之后,在缓存中生成一个计数器,第一次将这个计数器置1后存入缓存,并给其设定有效期。
每次点击后,取出这个值,计数器加一,如果超过限定次数,就抛出业务异常。
String limitURL =request.getRequestURI();//url 是Stringbuffer URI String
String key = user.getId()+limitURL+goodsId;
Integer count = redisService.get(MiaoshaKey.getMiaoshaFangShua,key,Integer.class);
if (count == null){
redisService.set(MiaoshaKey.getMiaoshaFangShua,key,1);//如果没有,说明没访问过,置1
}else if (count <5){//设置我们的防刷次数
redisService.incr(MiaoshaKey.getMiaoshaFangShua,key);//小于5 就+1
}else {//说明大于5
return Result.error(CodeMsg.REQUEST_OVER_LIMIT);
}
key 是获取的对应的用户 id+ url +商品id,值为count。
但是这样做有一个缺陷: 针对这个url 接口,我们可以防止刷固定次数,如果想要代码重用,使得其他url 接口也需要 限流防刷,那么就会每一次都要写一个这样的逻辑,并且不同的url 就不适用了。
如何做一个通用的限流防刷逻辑????
其实这是一层校验,不属于业务代码。
思路:
定义一个拦截器,利用拦截器来拦截这些请求,判断次数,进行操作。
方法:利用注解,自定义注解,并将注解打在我需要定义拦截器的方法上。
@AccessLimit(seconds = 5,maxCount = 5,needLogin = true)
1.新建注解:
Annotation 即 @interface
/**定义一个注解 : 用于 限流作用(在固定时间内限制访问次数)
* 降低代码复杂度和冗余度 提高复用性
* */
@Retention(RetentionPolicy.RUNTIME)//运行期间有效
@Target(ElementType.METHOD)//注解类型为方法注解
public @interface AccessLimit {
int seconds(); //固定时间
int maxCount();//最大访问次数
boolean needLogin() default true;// 用户是否需要登录
}
2.实现拦截器
/**用于实现 注解的 拦截器 需要实现HandlerInteceptorAdapter
* */
@Service
public class AccessInterceptor extends HandlerInterceptorAdapter {
@Autowired
MiaoshaUserService miaoshaUserService;
@Autowired
RedisService redisService;
/*改写这个方法,表示在方法执行之前拦截*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {//如果是HandlerMethod 类,强转,拿到注解
/*拿到用户*/
MiaoshaUser user = getUser(request,response);
/*为了方便实现user拦截器,存入当前user对象,这里直接就可以直接结合 登陆功能 做了*/
UserContext.setUser(user);
HandlerMethod hm = (HandlerMethod)handler;
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if (accessLimit == null){
return true;//没有注解 就放行表示执行完成
}
int maxCount = accessLimit.maxCount();//获取方法上注解的参数
int seconds = accessLimit.seconds();
boolean needLogin = accessLimit.needLogin();//判断登录 这里需要拿到用户User
/**由于之前只是通过拦截器 获取方法上的User变量,这里做一个拦截器来 判断用户是否登录
* 用到之前 UserArguementResolver中获得用户的代码
* */
String urlKey = request.getRequestURI();
/*第一部: 登陆验证*/
if (needLogin){//如果注解中 表示需要登录
if (user==null){//但是查不到用户
render(response,CodeMsg.SERVER_ERROR);//将错误码写入输出流输出出去
return false;//拦截 该方法,拦截器中只能 返回 true or false
}
//需要登录的拼上 用户id 来区别
urlKey+="_"+user.getId();
}else {
//do nothing! //不登录的就不拼
}
//第三部:访问时限设计,即定义缓存的生效时间 传入一个时间,获得一个有时间限制的前缀对象
MiaoshaKey ky = MiaoshaKey.withExpire(seconds);
//第二步:计数 限流逻辑
Integer count = redisService.get(ky,urlKey,Integer.class);
if (count == null){
redisService.set(ky,urlKey,1);//如果没有,说明没访问过,置1
}else if (count <maxCount){//设置 如果小于我们 的防刷次数
redisService.incr(ky,urlKey);//小于5 就+1
}else {//说明大于最大次数
render(response,CodeMsg.REQUEST_OVER_LIMIT);
return false;
}
return true;
}
return super.preHandle(request, response, handler);
}
/**render 方法为了 拦截的时候 输出到 浏览器,获得 response
* */
private void render(HttpServletResponse response, CodeMsg serverError) throws IOException {
/*注意 这里 输出的是 json 数据,所以 务必要定义 contentType 以及编码*/
response.setContentType("application/json;charset=utf-8");
OutputStream out = response.getOutputStream();
String str = JSON.toJSONString(Result.error(serverError));//转化为Json传输出
out.write(str.getBytes("UTF-8"));
out.flush();
out.close();
}
/**借用 获得用户的代码
* */
private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response){
String paramToken = request.getParameter(MiaoshaUserService.COOKIE_TOKEN_NAME);
String cookieToken = getCookieValue(request,MiaoshaUserService.COOKIE_TOKEN_NAME);
if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken))
{/*如果 cookie 中都没有值 返回 null 此时返回的 值 是给 MiaoshaUser 对象的 就是解析的参数值*/
return null;
}
/*有限从paramToken 中取出 cookie值 若没有从 cookieToken 中取*/
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
return miaoshaUserService.getByToken(response,token);/*拿到 user 对象*/
}
private String getCookieValue(HttpServletRequest request, String cookieTokenName) {
/*在 请求中 遍历所有的cookie 从中取到 我们需要的那一个cookie 就可以的*/
Cookie[] cookies = request.getCookies();
/*请求中没有cookies 的时候返回null ?? 没有cookie ? 没有登录吗?*/
if (cookies == null || cookies.length ==0)
{
return null;
}
for (Cookie cookie: cookies) {
if (cookie.getName().equals(cookieTokenName))
return cookie.getValue();
}
return null;
}
}
实现HandlerInterceptorAdapter 这个spring的拦截器基类。
通过实现这个接口,拿到方法上的注解。
于是我们需要三步 :1.判断是否登录 2.判断次数 3.判断固定时间(缓存时间)
1、判断登录,我们需要取到用户信息。
这里将之前原先定义在解析用户参数的代码,封装成一个活的用户信息的代码。然后在将这个用户信息,set到ThreadLocal 中,本地线程副本,该变量与线程绑定,存取只会存取在本地线程中。然后之前获取用户的代码直接取到该用户即可。
(拦截器先执行,解析参数在之后执行,而且,这个是在同一个线程中,所以用户就是本地用户。)
2.根据注解信息,若需要登录的,判断是否有用户信息,没有就返回,有就将url 拼接 用户 拼接起来。
然后判断访问次数count ,从缓存中存取,然后根据注解时间,设置缓存的过期时间。
注意:拦截出错的时候是将错误码CodeMsg 写入输出流,这里需要json 写出,以及编码是utf-8,以及是以bytes 写出。(不能是字符流) 不指定编码方式,输出是乱码
这里的AccessKey 是带有 过期时间的,即过期时间是需要传入的参数。
这就是为什么不用枚举类的一个原因,可以传值。???
3.将拦截器 注册到WebConfig中,这个类继承WebMvcConfigurerAdapter ,Spring框架的配置 类。