【任务13】数据统计与拦截:拦截恶意刷屏用户

本问题对应的项目链接:从0设计一个web项目

要求:
1、说一说你的大致处理逻辑
2、对于任何IP,在一分钟内访问超过 n 次,则拦截该IP,至于次数和时间范围,你们自己控制
3、进阶:支持白名单模式,就是IP在白名单里面的,则不受影响

共以下 5 个回答

  • 豆子彩虹 普通 2022年11月2日 上午9:04

    redis用set存储白名单,用ip:访问ip存储访问次数一分钟过期。访问所有资源的时候都会被该拦截器拦截,首先检查该ip是否在白名单中,如果在直接放行,如果不在的话,查看该ip一分钟内访问次数,如果访问次数在100以内,则放行,反之不允许访问。

    @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            String ip = request.getRemoteAddr();
            //判断当前ip地址是否在白名单中
            Boolean ipWhiteList = redisTemplate.opsForSet().isMember("ipWhiteList", ip);
            if (ipWhiteList) {
                return true;
            }
            String keyName = "ip:" + ip;
            String times = redisTemplate.opsForValue().get(keyName);
            //判断当前ip地址在一分钟内的访问次数
            if (times == null) {
                //该ip地址一分钟内未访问过
                redisTemplate.opsForValue().increment(keyName);
                redisTemplate.expire(keyName,1, TimeUnit.MINUTES);
            } else if (Integer.parseInt(times) < 100) {
                //该ip地址一分钟内访问次数小于100次
                redisTemplate.opsForValue().increment(keyName);
            } else {
                //该ip地址一分钟内访问次数超过100次
                return false;
            }
    
            return true;
        }
    

    github地址:https://github.com/lei342345795/FromZerotoExpert

    0 赞同 0 条回复
  • lucca 普通 2022年10月25日 下午4:53

    如何拦截恶意刷屏请求?

    使用 注解 + aop 拦截器的方式,实现接口级别的防刷校验

    对请求的唯一凭证(用户ID、token、sessionId都可以), 做请求数统计,存入redis,设置过期时间

    本项目选择 hash(请求路径 + sessionId) 作为 redis 的 key,访问次数做 value

    流程如下:

    1. 每次请求的时候,拦截器会拦截,先做黑白名单校验
    2. 如果上面两者都不是,做正常的请求次数判断
    3. 如果次数异常,会加入黑名单
    4. 请求次数正常,放行

    aop 的解决方案只有拦截器一种么?

    不是的,一共有三种:
    1. 过滤器 Filter:
    – 属于 servlet 标准
    – 需要配置匹配 url
    – 只有一个方法
    2. 拦截器 Interceptor:
    – 属于 spring 对 servlet 实现的内容
    – 需要配置匹配的 url
    – 三个方法,比上面更细致,可以在不同时间点做拓展
    3. aspectJ 切面:
    – 属于 spring 对 aop 的实践方案
    – 可以动态配置切点(aspectJ语法),比上面两者更加灵活
    – 多个方法,最细致,可以在不同时间点做拓展

    github 地址(点击即可)

    0 赞同 0 条回复
  • 小肸 永久会员 2022年10月25日 下午4:45

    对于白名单的处理,如果用redis缓存设置为永不过期,虽然查询速度快,但一个ip地址占4个字节,假设是大型网站,用户有1个亿吧,白名单存2000万,需要8000万字节,差不多76MB。对于现在8G的内存来说,可以接受,所以用redis存储,用redis的列表类型,另外可以提供几个接口动态维护白名单。拦截IP的话,当一个IP第一次请求时,创建一个IP+访问次数的键值对,过期数据设置为1分钟,当这个IP第二次请求时,直接将对应的访问次数加1,看它超过了10次没,超过了不容许访问,直到redis里对应的键值对过期,回到第一次访问的状态。这个逻辑可以封装成一个函数,在拦截器里面判断。访问次数和时间限制可以根据实际情况设置。

    @Component
    public class MyInterceptor implements HandlerInterceptor {
        private final Integer limitIpCount = 10;
        private final Integer limitIpTime = 60;
        @Resource
        private RedisTemplate redisTemplate;
        //返回值为false,则说明禁止访问
        //返回值为true,则说明允许访问
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 设置输出流编码格式,支持中文
            response.setHeader("Content-type", "text/html;charset=UTF-8");
            response.setCharacterEncoding("UTF-8");
            countWebsiteData(request, response);
            if (limitIp(request)) {
                response.getWriter().write(JSONObject.toJSONString(ResultVOUtil.fail(401,"访问次数过多,请一分钟后重试")));
                return false;
            }
            //不拦截路径(登录路径等等)
            List asList = Arrays.asList("/fromzerotoexpert/register", "/fromzerotoexpert/login", "/fromzerotoexpert/FromZerotoExpert");
            String uri = request.getRequestURI();
            //1.设置放行路径
            if(asList.contains(uri)){
                return true;
            }
            if (getCookie(request, "accountId") != null) {
                Cookie cookie = getCookie(request, "accountId");
                String accountId = cookie.getValue();
                String sessionId = (String) redisTemplate.opsForList().index(accountId, 0);
    //            if (sessionId == null) {
    //                response.getWriter().write(JSONObject.toJSONString(ResultVOUtil.fail(401,"请先登录")));
    //                return false;
    //            }
                if (sessionId.equals(request.getSession().getId())) {
                    return true;
                } else {
                    response.getWriter().write(JSONObject.toJSONString(ResultVOUtil.fail(401,"你的账号已在其它地方登录")));
                    return false;
                }
            }
    //        System.out.println(JSONObject.toJSONString(ResultVOUtil.fail(401,"请先登录")));
            response.getWriter().write(JSONObject.toJSONString(ResultVOUtil.fail(401,"请先登录")));
            System.out.println("方法执行前.......");
            return false;
        }
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            System.out.println("方法执行后.......");
        }
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            System.out.println("清理.......");
        }
        // 更新网站当日的访问数据并向前端发送通知
        private void countWebsiteData(HttpServletRequest request, HttpServletResponse response) {
            // 当前日期 (只有日期没有时间)
            String curDate = LocalDate.now().format(DateTimeFormatter.ofPattern("dd-MM-yyyy"));
            if (redisTemplate.opsForHash().get(curDate, "ip") == null) {
                redisTemplate.opsForHash().put(curDate, "ip", 0);
            }
            if (redisTemplate.opsForHash().get(curDate, "uv") == null) {
                redisTemplate.opsForHash().put(curDate, "uv", 0);
            }
            if (redisTemplate.opsForHash().get(curDate, "pv") == null) {
                redisTemplate.opsForHash().put(curDate, "pv", 0);
            }
            int newIpCount = (int) redisTemplate.opsForHash().get(curDate, "ip");
            int newUvCount = (int) redisTemplate.opsForHash().get(curDate, "uv");
            int newPvCount = (int) redisTemplate.opsForHash().get(curDate, "pv");
            // 调用IPUtil类的getIpAdress()方法获取请求真正的ip地址
            String curIp = IPUtil.getIpAdrress(request);
            // 如果是个新的ip地址,更新ip集合和newIp的值
            if (!redisTemplate.opsForSet().isMember("ip", curIp)) {
                redisTemplate.opsForSet().add("ip", curIp);
                newIpCount ++;
            }
            // 获取当前时间到第二天凌晨时间的秒数
            long targetSeconds = MyTimeUtil.getNowToNextDaySeconds();
            // 获取请求中Cookie键为UID的值
            if (getCookie(request, "UID") == null) {
                newUvCount ++;
                Cookie cookie = new Cookie("UID", "UID");
                cookie.setMaxAge((int) targetSeconds);
                response.addCookie(cookie);
            }
            newPvCount ++;
            redisTemplate.opsForHash().put(curDate, "ip", newIpCount);
            redisTemplate.opsForHash().put(curDate, "uv", newUvCount);
            redisTemplate.opsForHash().put(curDate, "pv", newPvCount);
            // 把过期时间设置长一些,以便定时统计
            redisTemplate.expire(curDate, targetSeconds + 5 * 60, TimeUnit.SECONDS);
            WebsiteDataForm websiteDataForm = new WebsiteDataForm();
            websiteDataForm.setDate(curDate);
            websiteDataForm.setIp(newIpCount);
            websiteDataForm.setUv(newUvCount);
            websiteDataForm.setPv(newPvCount);
            // 向前端发送新的序列化后的网站访问数据
            Websocket.sendMessage(JSONObject.toJSONString(websiteDataForm));
        }
    
        /**
         *
         * @param request
         * @return 请求是否有效(防刷屏),true表示请求受限
         */
        private boolean limitIp(HttpServletRequest request) {
            String curIp = IPUtil.getIpAdrress(request);
            // TODO 这里可以添加判断白名单操作
            if (redisTemplate.opsForValue().get(curIp) == null) {
                redisTemplate.opsForValue().set(curIp, 1);
                // 过期时间设为1分钟
                redisTemplate.expire(curIp, limitIpTime, TimeUnit.SECONDS);
            } else {
                Integer newIpCount = (int) redisTemplate.opsForValue().get(curIp) + 1;
                // 一分钟内访问次数超过10次
                if (newIpCount > limitIpCount) {
                    return true;
                }
                redisTemplate.opsForValue().set(curIp, newIpCount);
            }
            return false;
        }
    }
    
    0 赞同 0 条回复
  • 秊 永久会员 2022年10月13日 下午3:11
    1. 设置某个接口的访问次数限制:
      • 设计一个注释类 VisitLimit :sec 时间内只能访问 limit 次(我这里设置的是10秒钟只能访问2次)
      • 在拦截器中判断:请求的IP地址是否在白名单内,在则直接跳过限制的判断; 不在则判断拦截的请求对应的接口是否具有 VisitLimit 注解,进行相关判断
    2. 用户白名单设计:
      • 使用 redis 中的 set, key为 [fU:wI],value为[用户IP+访问的接口] 查询复杂度 $O(1)$
      • 增加程序运行过程中动态增删用户白名单接口 /AddUserWhiteIP,/DelUserWhiteIP
    3. 部分实现代码如下:
    // annotation/VisitLimit
    /**
     * 接口限流
     */
    @Inherited
    @Documented
    @Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface VisitLimit {
    
        // 标识: 指定 sec 时间段内访问次数的限制
        int limit() default 1;
    
        // 标识: 时间段
        long sec() default  5;
    }
    
    // 首页接口增加 VisitLimit注解
    /**
         * 首页
         */
        @VisitLimit(limit = 2, sec = 10)
        @GetMapping("/FromZerotoExpert")
        public String fromZerotoExpert() {
            return "FromZerotoExpert";
        }
    
    //intercepter
    /**
         * 接口访问限制
         */
        private boolean isVisitLimit(HttpServletRequest request, HttpServletResponse response, Object handler, Jedis jedis) throws IOException {
            String userIP= IpUtils.getIpAddr(request);
            String userIPAndURL = userIP + request.getRequestURI();
    
            if (handler instanceof HandlerMethod) {
    
                // 判断用户IP在白名单中, 直接返回
                if (jedis.sismember("fU:wI", userIP)) {
                    return false;
                }
    
                // 不再名单中则进行接口限制判断
                HandlerMethod handlerMethod = (HandlerMethod) handler;
                Method method = handlerMethod.getMethod();
                // 访问的接口中是否含有[访问次数限制]的注解
                if (method.isAnnotationPresent(VisitLimit.class)) {
                    VisitLimit accessLimit = method.getAnnotation(VisitLimit.class);
                    int limit = accessLimit.limit();
                    long sec = accessLimit.sec();
                    Integer value = null;
                    if (jedis.exists(userIPAndURL)) {  // 用户在 sec 期间已经访问过吗
                        value = Integer.valueOf(String.valueOf(jedis.get(userIPAndURL)));
                        if (value < limit) {  // 判断访问次数是否超限
                            jedis.setex(userIPAndURL, sec, String.valueOf(value + 1));
                        } else {
                            // 请求太繁忙
                            response.getWriter().write("Requests are busy!");
                            return true;
                        }
                    } else {
                        jedis.setex(userIPAndURL, sec, "1");
                    }
                }
            }
            return false;
        }
    
    // controller (删除同理)
    /**
         * 增加用户白名单
         * @return 返回添加成功的个数; "-1"--添加失败
         */
        @PostMapping("/AddUserWhiteIP")
        @ResponseBody
        public String addUserWhiteIP(String key) {
            long res = -1L;
            try(Jedis jedis = userService.jedisPool.getResource()) {
                res = jedis.sadd("fU:wI", key);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return Long.toString(res);
        }
    
    

    服务器链接:http://124.223.184.187:8088/FromZerotoExpert
    Github仓库链接:https://github.com/jiangyklala/FromZerotoExpert/tree/dev

    0 赞同 0 条回复
  • Second(S) 普通 2022年10月13日 上午12:28

    1.处理逻辑
    在IP拦截器中进行拦截,获取访问者的IP,并在redis库中按如下顺序进行搜索:
    ①若此IP在白名单内,直接放行
    ②若此IP在黑名单内,不放行,并重定向至黑名单专享页面。
    ③若既不在白名单内,又不在黑名单内,则进行此IP的访问次数统计,其中在redis中的存储数据结构为String类型,key为cout:ip:+真实ip,值为访问次数,存活时间为1分钟。若访问时redis数据库中无此key,则在redis数据库中设置对应的key,并将值设为0。若访问时redis数据库中存在此key,则将访问次数加1(加1操作不会影响存活时间),若访问次数累计值超过100,则设置此ip为黑名单ip,并重定向至黑名单页面,加入黑名单的ip在访问所有资源时,都不可用。
    注1:1分钟后,key会因为到了过期时间而失效,因此再次访问时会被重新归零。
    注2:黑名单在redis存储的数据类型为String类型,key为black:ip:+真实ip,值为固定字符串。
    注3:白名单应该在mysql中存储,并在服务器运行时动态加载到redis数据库中,因为自己IP个数有限,不好测试,就没继续开发此功能。

    IP拦截器部分代码如下:

                // 检测是否在白名单内
                if(stringRedisTemplate.hasKey(PREFIX_WHITE_IP + ip)){
                    return true;
                }
                // 检测是否在黑名单内
                if(stringRedisTemplate.hasKey(PREFIX_BLACK_IP + ip)){
                    response.sendRedirect("/blackHouse");
                    return false;
                }
    
                // IP访问计数
                String count_key = PREFIX_KEY_COUNT_IP + ip;
                if(!stringRedisTemplate.hasKey(count_key)){
                    stringRedisTemplate.opsForValue().set(count_key,"0");
                    stringRedisTemplate.expire(count_key, 60, TimeUnit.SECONDS);
                }
                stringRedisTemplate.opsForValue().increment(count_key);
                Integer count = Integer.parseInt(stringRedisTemplate.opsForValue().get(count_key));
    
                if(count > 100){
                    // 访问频率过高,加入黑名单
                    stringRedisTemplate.opsForValue().set(PREFIX_BLACK_IP + ip, "blackHouse");
                    stringRedisTemplate.expire(PREFIX_BLACK_IP + ip, 60, TimeUnit.SECONDS);
                    return false;
                }else{
                    return true;
                }
            }catch (Exception e){
                System.out.println("IP拦截器拦截!");
                e.printStackTrace();
                return false;
            }
    

    2.在这里设置的是1分钟内访问次数超过100次对IP进行拦截。因为测试时不容易模拟出1分钟超过100次的请求,所以在这里提供了一个按钮,点击就相当于用户访问次数过多直接被拦截。等待一分钟后即可正常操作。

    gitHub地址:https://github.com/Second6s9/FromZerotoExpert/tree/dev
    gitHub防覆盖地址:https://github.com/Second6s9/FromZerotoExpert/tree/day13
    服务器地址:http://123.57.142.33:8088/FromZerotoExpert/

    0 赞同 0 条回复

# 回答此问题

后才能回答