一个简单IP防刷工具类, 10分钟内只最多允许1000次单ip用户操作

栏目: 数据库 · 发布时间: 5年前

内容简介:IP防刷,也就是在短时间内有大量相同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. 使用切面的方式进行请求拦截,避免代码入侵;


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Tagging

Tagging

Gene Smith / New Riders / 2007-12-27 / GBP 28.99

Tagging is fast becoming one of the primary ways people organize and manage digital information. Tagging complements traditional organizational tools like folders and search on users desktops as well ......一起来看看 《Tagging》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试