安全优化---接口限流防刷

接口限流防刷:

限制同一个用户在限定时间内,只能访问固定次数。

思路:每次点击之后,在缓存中生成一个计数器,第一次将这个计数器置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框架的配置 类。

猜你喜欢

转载自blog.csdn.net/weixin_38035852/article/details/81192580