一個簡單IP防刷工具類, 10分鐘內只最多允許1000次單ip用戶操作

收藏待读

一個簡單IP防刷工具類, 10分鐘內只最多允許1000次單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  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. 使用切面的方式進行請求攔截,避免代碼入侵;

原文 : 等你歸去來

相關閱讀

免责声明:本文内容来源于博客園-原創精華區,已注明原文出处和链接,文章观点不代表立场,如若侵犯到您的权益,或涉不实谣言,敬请向我们提出检举。