zoukankan      html  css  js  c++  java
  • 一个简单IP防刷工具类, x秒内最多允许y次单ip操作

      IP防刷,也就是在短时间内有大量相同ip的请求,可能是恶意的,也可能是超出业务范围的。总之,我们需要杜绝短时间内大量请求的问题,怎么处理?

      其实这个问题,真的是太常见和太简单了,但是真正来做的时候,可能就不一定很简单了哦。

      我这里给一个解决方案,以供参考!

    主要思路或者需要考虑的问题为:

      1. 因为现在的服务器环境几乎都是分布式环境,所以,用本地计数的方式肯定是不行了,所以我们需要一个第三方的工具来辅助计数;

      2. 可以选用数据库、缓存中间件、zk等组件来解决分布式计数问题;

      3. 使用自增计数,尽量保持原子性,避免误差;

      4. 统计周期为从当前倒推 interval 时间,还是直接以某个开始时间计数;

      5. 在何处进行拦截? 每个方法开始前? 还是请求入口处?

    实现代码示例如下:

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.web.context.request.RequestAttributes;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    import redis.clients.jedis.Jedis;
    
    import javax.annotation.Resource;
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * IP 防刷工具类, 10分钟内只最多允许1000次用户操作
     */
    @Aspect
    public class IpFlushFirewall {
    
        @Resource
        private Jedis redisTemplate;
    
        /**
         * 最大ip限制次数
         */
        private static int maxLimitIpHit = 1000;
    
        /**
         * 检查时效,单位:秒
         */
        private static int checkLimitIpHitInterval = 600;
    
        // 自测试有效性
        public static void main(String[] args) {
            IpFlushFirewall ipTest = new IpFlushFirewall();
            // 测试时直接使用new Jedis(), 正式运行时使用 redis-data 组件配置即可
            ipTest.redisTemplate = new Jedis("127.0.0.1", 6379);
            for (int i = 0; i < 10; i++) {
                System.out.println("new action: +" + i);
                ipTest.testLoginAction(new Object());
                System.out.println("action: +" + i + ", passed...");
            }
        }
    
        // 测试访问的方法
        public Object testLoginAction(Object req) {
            // ip防刷
            String reqIp = "127.0.0.1";
            checkIpLimit(reqIp);
            // 用户信息校验
            System.out.println("login success...");
            // 返回用户信息
            return null;
        }
    
        // 检测限制入口
        public void checkIpLimit(String ip) {
            if(isIpLimited(ip)) {
                throw new RuntimeException("操作频繁,请稍后再试!");
            }
        }
    
        // ip 防刷 / 使用切面进行拦截
        @Before(value = "execution(public * com.*.*.*(..))")
        public void checkIpLimit() {
            RequestAttributes ra = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes sra = (ServletRequestAttributes) ra;
            HttpServletRequest request = sra.getRequest();
            String ip = getIp(request);
            if(isIpLimited(ip)) {
                throw new RuntimeException("操作频繁,请稍后再试!");
            }
        }
    
        public static String getIp(HttpServletRequest request) {
            String ip = request.getHeader("x-forwarded-for");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
            // 多级代理问题
            if(ip.contains(",")) {
                ip = ip.substring(0, ip.indexOf(',')).trim();
            }
            return ip;
        }
    
        /**
         * 判断ip是否受限制, 非核心场景,对于非原子的更新计数问题不大,否则考虑使用分布式锁调用更新
         */
        private boolean isIpLimited(String reqIp) {
            String ipHitCache = getIpHitCacheKey(reqIp);
            // 先取旧数据作为本次判断,再记录本次访问
            String hitsStr = redisTemplate.get(ipHitCache);
            recordNewIpRequest(reqIp);
            // 新周期内,首次访问
            if(hitsStr == null) {
                return false;
            }
            // 之前有命中
            // 总数未超限,直接通过
            if(!isOverMaxLimit(Integer.valueOf(hitsStr) + 1)) {
                return false;
            }
            // 当前访问后超过限制后,再判断周期内的数据
            Long retainIpHits = countEffectiveIntervalIpHit(reqIp);
            redisTemplate.set(ipHitCache, retainIpHits + "");
            // 将有效计数更新回计数器,删除无效计数后,在限制范围内,则不限制操作
            if(!isOverMaxLimit(retainIpHits.intValue())) {
                return false;
            }
            return true;
        }
    
        // 是否超过最大限制
        private boolean isOverMaxLimit(Integer nowCount) {
            return nowCount > maxLimitIpHit;
        }
    
        // 每次访问必须记录
        private void recordNewIpRequest(String reqIp) {
            if(redisTemplate.exists(getIpHitCacheKey(reqIp))) {
                // 自增访问量
                redisTemplate.incr(getIpHitCacheKey(reqIp));
            }
            else {
                redisTemplate.set(getIpHitCacheKey(reqIp), "1");
            }
            redisTemplate.expire(getIpHitCacheKey(reqIp), checkLimitIpHitInterval);
            Long nowTime = System.currentTimeMillis() / 1000;
            // 使用 sorted set 保存记录时间,方便删除, zset 元素尽可能保持唯一,否则会导致统计有效时数据变少问题
            redisTemplate.zadd(getIpHitStartTimeCacheKey(reqIp), nowTime , reqIp + "-" + System.nanoTime() + Math.random());
            redisTemplate.expire(getIpHitStartTimeCacheKey(reqIp), checkLimitIpHitInterval);
        }
    
        /**
         * 统计计数周期内有效的的访问次数(删除无效统计)
         *
         * @param reqIp 请求ip
         * @return 有效计数
         */
        private Long countEffectiveIntervalIpHit(String reqIp) {
            // 删除统计周期外的计数
            Long nowTime = System.currentTimeMillis() / 1000;
            redisTemplate.zremrangeByScore(getIpHitStartTimeCacheKey(reqIp), nowTime - checkLimitIpHitInterval, nowTime);
            return redisTemplate.zcard(getIpHitStartTimeCacheKey(reqIp));
        }
    
        // ip 访问计数器缓存key
        private String getIpHitCacheKey(String reqIp) {
            return "secure.ip.limit." + reqIp;
        }
    
        // ip 访问开始时间缓存key
        private String getIpHitStartTimeCacheKey(String reqIp) {
            return "secure.ip.limit." + reqIp + ".starttime";
        }
    
    }

      如上解决思路为:

        1. 使用 redis 做计数器工具,做到数据统一的同时,redis 的高性能特性也保证了整个应用性能;

        2. 使用 redis 的 incr 做自增,使用一个 zset 来保存记录开始时间,做双重保险;

        3. 在计数超过限制后,再做开始有效性的检测,保证准确的同时,避免了每次都手动检查有时间有效性的动作;

                4. 正常的统计周期超时,借助redis自动淘汰机制清理,无需手动管理;

        5. 使用切面的方式进行请求拦截,避免业务代码入侵;

  • 相关阅读:
    我爱java系列之---【微服务间的认证—Feign拦截器】
    我爱java系列之---【设置权限的三种解决方案】
    581. Shortest Unsorted Continuous Subarray
    129. Sum Root to Leaf Numbers
    513. Find Bottom Left Tree Value
    515. Find Largest Value in Each Tree Row
    155. Min Stack max stack Maxpop O(1) 操作
    painting house
    Minimum Adjustment Cost
    k Sum
  • 原文地址:https://www.cnblogs.com/yougewe/p/10256532.html
Copyright © 2011-2022 走看看