欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Java分布式IP限流和防止惡意IP攻擊方案

 更新時(shí)間:2020年03月30日 09:24:03   作者:胡峻崢  
這篇文章主要介紹了詳解Java分布式IP限流和防止惡意IP攻擊方案,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

前言

限流是分布式系統(tǒng)設(shè)計(jì)中經(jīng)常提到的概念,在某些要求不嚴(yán)格的場景下,使用Guava RateLimiter就可以滿足。但是Guava RateLimiter只能應(yīng)用于單進(jìn)程,多進(jìn)程間協(xié)同控制便無能為力。本文介紹一種簡單的處理方式,用于分布式環(huán)境下接口調(diào)用頻次管控。

如何防止惡意IP攻擊某些暴露的接口呢(比如某些場景下短信驗(yàn)證碼服務(wù))?本文介紹一種本地緩存和分布式緩存集成方式判斷遠(yuǎn)程IP是否為惡意調(diào)用接口的IP。

分布式IP限流

思路是使用redis incr命令,完成一段時(shí)間內(nèi)接口請求次數(shù)的統(tǒng)計(jì),以此來完成限流相關(guān)邏輯。

private static final String LIMIT_LUA =
  "local my_limit = redis.call('incr', KEYS[1])\n" +
      " if tonumber(my_limit) == 1 then\n" +
      "  redis.call('expire', KEYS[1], ARGV[1])\n" +
      "  return 1\n" +
      " elseif tonumber(my_limit) > tonumber(ARGV[2]) then\n" +
      "  return 0\n" +
      " else\n" +
      "  return 1\n" +
      " end\n";

這里為啥時(shí)候用lua腳本來實(shí)現(xiàn)呢?因?yàn)橐WCincr命令和expire命令的原子性操作。KEYS[1]代表自增key值, ARGV[1]代表過期時(shí)間,ARGV[2]代表最大頻次,明白了這些參數(shù)的含義,整個(gè)lua腳本邏輯也就不言而喻了。

/**
 * @param limitKey 限制Key值
 * @param maxRate 最大速率
 * @param expire  Key過期時(shí)間
 */
public boolean access(String limitKey, int maxRate, int expire) {
  if (StringUtils.isBlank(limitKey)) {
    return true;
  }

  String cacheKey = LIMIT_KEY_PREFIX + limitKey;

  return REDIS_SUCCESS_STATUS.equals(
      this.cacheService.eval(
          LIMIT_LUA
          , Arrays.asList(cacheKey)
          , Arrays.asList(String.valueOf(expire), String.valueOf(maxRate))
      ).toString()
  );
}

public void unlimit(String limitKey) {
  if (StringUtils.isBlank(limitKey)) {
    return;
  }
  String cacheKey = LIMIT_KEY_PREFIX + limitKey;
  this.cacheService.decr(cacheKey);
}

access方法用來判斷 limitKey 是否超過了最大訪問頻次。緩存服務(wù)對象(cacheService)的eval方法參數(shù)分別是lua腳本、key list、value list。

unlimit方法其實(shí)就是執(zhí)行redis decr操作,在某些業(yè)務(wù)場景可以回退訪問頻次統(tǒng)計(jì)。

防止惡意IP攻擊

由于某些對外暴露的接口很容易被惡意用戶攻擊,必須做好防范措施。最近我就遇到了這么一種情況,我們一個(gè)快應(yīng)用產(chǎn)品,短信驗(yàn)證碼服務(wù)被惡意調(diào)用了。通過后臺的日志發(fā)現(xiàn),IP固定,接口調(diào)用時(shí)間間隔固定,明顯是被人利用了。雖然我們針對每個(gè)手機(jī)號每天發(fā)送短信驗(yàn)證碼的次數(shù)限制在5次以內(nèi)。但是短信驗(yàn)證碼服務(wù)每天這樣被重復(fù)調(diào)用,會打擾用戶并產(chǎn)生投訴。針對這種現(xiàn)象,簡單的做了一個(gè)方案,可以自動識別惡意攻擊的IP并加入黑名單。

思路是這樣的,針對某些業(yè)務(wù)場景,約定在一段時(shí)間內(nèi)同一個(gè)IP訪問最大頻次,如果超過了這個(gè)最大頻次,那么就認(rèn)為是非法IP。識別了非法IP后,把IP同時(shí)放入本地緩存和分布式緩存中。非法IP再次訪問的時(shí)候,攔截器發(fā)現(xiàn)本地緩存(沒有則去分布式緩存)有記錄這個(gè)IP,直接返回異常狀態(tài),不會繼續(xù)執(zhí)行正常業(yè)務(wù)邏輯。

Guava本地緩存集成Redis分布式緩存

public abstract class AbstractCombineCache<K, V> {
  private static Logger LOGGER = LoggerFactory.getLogger(AbstractCombineCache.class);

  protected Cache<K, V> localCache;

  protected ICacheService cacheService;

  public AbstractCombineCache(Cache<K, V> localCache, ICacheService cacheService) {
    this.localCache = localCache;
    this.cacheService = cacheService;
  }

  public Cache<K, V> getLocalCache() {
    return localCache;
  }

  public ICacheService getCacheService() {
    return cacheService;
  }

  public V get(K key) {
    //只有LoadingCache對象才有g(shù)et方法,如果本地緩存不存在key值, 會執(zhí)行CacheLoader的load方法,從分布式緩存中加載。
    if (localCache instanceof LoadingCache) {
      try {
        return ((LoadingCache<K, V>) localCache).get(key);
      } catch (ExecutionException e) {
        LOGGER.error(String.format("cache key=%s loading error...", key), e);
        return null;
      } catch (CacheLoader.InvalidCacheLoadException e) {
        //分布式緩存中不存在這個(gè)key
        LOGGER.error(String.format("cache key=%s loading fail...", key));
        return null;
      }
    } else {
      return localCache.getIfPresent(key);
    }
  }

  public void put(K key, V value, int expire) {
    this.localCache.put(key, value);
    String cacheKey = key instanceof String ? (String) key : key.toString();
    if (value instanceof String) {
      this.cacheService.setex(cacheKey, (String) value, expire);
    } else {
      this.cacheService.setexObject(cacheKey, value, expire);
    }
  }
}

AbstractCombineCache這個(gè)抽象類封裝了guava本地緩存和redis分布式緩存操作,可以降低分布式緩存壓力。

防止惡意IP攻擊緩存服務(wù)

public class IPBlackCache extends AbstractCombineCache<String, Object> {
  private static Logger LOGGER = LoggerFactory.getLogger(IPBlackCache.class);

  private static final String IP_BLACK_KEY_PREFIX = "wmhipblack_";

  private static final String REDIS_SUCCESS_STATUS = "1";

  private static final String IP_RATE_LUA =
      "local ip_rate = redis.call('incr', KEYS[1])\n" +
          " if tonumber(ip_rate) == 1 then\n" +
          "  redis.call('expire', KEYS[1], ARGV[1])\n" +
          "  return 1\n" +
          " elseif tonumber(ip_rate) > tonumber(ARGV[2]) then\n" +
          "  return 0\n" +
          " else\n" +
          "  return 1\n" +
          " end\n";

  public IPBlackCache(Cache<String, Object> localCache, ICacheService cacheService) {
    super(localCache, cacheService);
  }

  /**
   * @param ipKey  IP
   * @param maxRate 最大速率
   * @param expire 過期時(shí)間
   */
  public boolean ipAccess(String ipKey, int maxRate, int expire) {
    if (StringUtils.isBlank(ipKey)) {
      return true;
    }

    String cacheKey = IP_BLACK_KEY_PREFIX + ipKey;

    return REDIS_SUCCESS_STATUS.equals(
        this.cacheService.eval(
            IP_RATE_LUA
            , Arrays.asList(cacheKey)
            , Arrays.asList(String.valueOf(expire), String.valueOf(maxRate))
        ).toString()
    );
  }

  /**
   * @param ipKey IP
   */
  public void removeIpAccess(String ipKey) {
    if (StringUtils.isBlank(ipKey)) {
      return;
    }
    String cacheKey = IP_BLACK_KEY_PREFIX + ipKey;
    try {
      this.cacheService.del(cacheKey);
    } catch (Exception e) {
      LOGGER.error(String.format("%s, ip access remove error...", ipKey), e);
    }
  }
}

沒有錯(cuò),IP_RATE_LUA 這個(gè)lua腳本和上面說的限流方案對應(yīng)的lua腳本是一樣的。

IPBlackCache繼承了AbstractCombineCache,構(gòu)造函數(shù)需要guava的本地Cache對象和redis分布式緩存服務(wù)ICacheService 對象。

ipAccess方法用來判斷當(dāng)前ip訪問次數(shù)是否在一定時(shí)間內(nèi)已經(jīng)達(dá)到了最大訪問頻次。

removeIpAccess方法是直接移除當(dāng)前ip訪問頻次統(tǒng)計(jì)的key值。

防止惡意IP攻擊緩存配置類

@Configuration
public class IPBlackCacheConfig {
  private static final String IPBLACK_LOCAL_CACHE_NAME = "ip-black-cache";
  private static Logger LOGGER = LoggerFactory.getLogger(IPBlackCacheConfig.class);

  @Autowired
  private LimitConstants limitConstants;

  @Bean
  public IPBlackCache ipBlackCache(@Autowired ICacheService cacheService) {
    GuavaCacheBuilder cacheBuilder = new GuavaCacheBuilder<String, Object>(IPBLACK_LOCAL_CACHE_NAME);
    cacheBuilder.setCacheBuilder(
        CacheBuilder.newBuilder()
            .initialCapacity(100)
            .maximumSize(10000)
            .concurrencyLevel(10)
            .expireAfterWrite(limitConstants.getIpBlackExpire(), TimeUnit.SECONDS)
            .removalListener((RemovalListener<String, Object>) notification -> {
              String curTime = LocalDateTime.now().toString();
              LOGGER.info(notification.getKey() + " 本地緩存移除時(shí)間:" + curTime);
              try {
                cacheService.del(notification.getKey());
                LOGGER.info(notification.getKey() + " 分布式緩存移除時(shí)間:" + curTime);
              } catch (Exception e) {
                LOGGER.error(notification.getKey() + " 分布式緩存移除異常...", e);
              }
            })
    );
    cacheBuilder.setCacheLoader(new CacheLoader<String, Object>() {
      @Override
      public Object load(String key) {
        try {
          Object obj = cacheService.getString(key);
          LOGGER.info(String.format("從分布式緩存中加載key=%s, value=%s", key, obj));
          return obj;
        } catch (Exception e) {
          LOGGER.error(key + " 從分布式緩存加載異常...", e);
          return null;
        }
      }
    });

    Cache<String, Object> localCache = cacheBuilder.build();
    IPBlackCache ipBlackCache = new IPBlackCache(localCache, cacheService);
    return ipBlackCache;
  }
}

注入redis分布式緩存服務(wù)ICacheService對象。

通過GuavaCacheBuilder構(gòu)建guava本地Cache對象,指定初始容量(initialCapacity)、最大容量(maximumSize)、并發(fā)級別、key過期時(shí)間、key移除監(jiān)聽器。最終要的是CacheLoader這個(gè)參數(shù),是干什么用的呢?如果GuavaCacheBuilder指定了CacheLoader對象,那么最終創(chuàng)建的guava本地Cache對象是LoadingCache類型(參考AbstractCombineCache類的get方法),LoadingCache對象的get方法首先從內(nèi)存中獲取key對應(yīng)的value,如果內(nèi)存中不存在這個(gè)key則調(diào)用CacheLoader對象的load方法加載key對應(yīng)的value值,加載成功后放入內(nèi)存中。

最后通過ICacheService對象和guava本地Cache對象創(chuàng)建IPBlackCache(防止惡意IP攻擊緩存服務(wù))對象。

攔截器里惡意IP校驗(yàn)

定義一個(gè)注解,標(biāo)注在指定方法上,攔截器里會識別這個(gè)注解。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IPBlackLimit {
  //統(tǒng)計(jì)時(shí)間內(nèi)最大速率
  int maxRate();

  //頻次統(tǒng)計(jì)時(shí)間
  int duration();

  //方法名稱
  String method() default StringUtils.EMPTY;
}

攔截器里加入ipAccess方法,校驗(yàn)遠(yuǎn)程IP是否為惡意攻擊的IP。

/**
* @param method 需要校驗(yàn)的方法
* @param remoteAddr 遠(yuǎn)程IP
*/
private boolean ipAccess(Method method, String remoteAddr) {
  if (StringUtils.isBlank(remoteAddr) || !AnnotatedElementUtils.isAnnotated(method, IPBlackLimit.class)) {
    return true;
  }
  IPBlackLimit ipBlackLimit = AnnotatedElementUtils.getMergedAnnotation(method, IPBlackLimit.class);
  try {
    String ip = remoteAddr.split(",")[0].trim();
    String cacheKey = "cipb_" + (StringUtils.isBlank(ipBlackLimit.method()) ? ip : String.format("%s_%s", ip, ipBlackLimit.method()));

    String beginAccessTime = (String) ipBlackCache.get(cacheKey);
    if (StringUtils.isNotBlank(beginAccessTime)) {
      LocalDateTime beginTime = LocalDateTime.parse(beginAccessTime, DateTimeFormatter.ISO_LOCAL_DATE_TIME), endTime = LocalDateTime.now();
      Duration duration = Duration.between(beginTime, endTime);
      if (duration.getSeconds() >= limitConstants.getIpBlackExpire()) {
        ipBlackCache.getLocalCache().invalidate(cacheKey);
        return true;
      } else {
        return false;
      }
    }

    boolean access = ipBlackCache.ipAccess(cacheKey, ipBlackLimit.maxRate(), ipBlackLimit.duration());
    if (!access) {
      ipBlackCache.removeIpAccess(cacheKey);
      String curTime = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
      ipBlackCache.put(cacheKey, curTime, limitConstants.getIpBlackExpire());
    }
    return access;
  } catch (Exception e) {
    LOGGER.error(String.format("method=%sï¼remoteAddr=%s, ip access check error.", method.getName(), remoteAddr), e);
    return true;
  }
}

remoteAddr取的是X-Forwarded-For對應(yīng)的值。利用 remoteAddr 構(gòu)造 cacheKey 參數(shù),通過IPBlackCache判斷 cacheKey 是否存在。

如果是 cacheKey 存在的請求,判斷黑名單IP限制是否已經(jīng)到達(dá)有效期,如果已經(jīng)超過有效期則清除本地緩存和分布式緩存的 cacheKey ,請求合法;如果沒有超過有效期則請求非法。

否則是 cacheKey 不存在的請求,使用IPBlackCache對象的ipAccess方法統(tǒng)計(jì)一定時(shí)間內(nèi)的訪問頻次,如果頻次超過最大限制,表明是非法請求IP,需要往IPBlackCache對象寫入“ cacheKey =當(dāng)前時(shí)間”。

總結(jié)

本文的兩種方案都使用redis incr命令,如果不是特殊業(yè)務(wù)場景,redis的key要指定過期時(shí)間,嚴(yán)格來講需要保證incr和expire兩個(gè)命令的原子性,所以使用lua腳本方式。如果沒有那么嚴(yán)格,完全可以先setex(設(shè)置key,value,過期時(shí)間),然后再incr(注: incr不會更新key的有效期 )。本文的設(shè)計(jì)方案僅供參考,并不能應(yīng)用于所有的業(yè)務(wù)場景。

到此這篇關(guān)于詳解Java分布式IP限流和防止惡意IP攻擊方案的文章就介紹到這了,更多相關(guān)Java 分布式IP限流和防止惡意IP內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Java的枚舉enum示例詳解

    Java的枚舉enum示例詳解

    這篇文章主要給大家介紹了關(guān)于Java的枚舉enum的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用Java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-08-08
  • JAVA8 List<List<Integer>> list中再裝一個(gè)list轉(zhuǎn)成一個(gè)list操作

    JAVA8 List<List<Integer>> list中再裝一個(gè)list轉(zhuǎn)成一個(gè)list操

    這篇文章主要介紹了JAVA8 List<List<Integer>> list中再裝一個(gè)list轉(zhuǎn)成一個(gè)list操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-08-08
  • Java中接口和抽象類的區(qū)別詳解

    Java中接口和抽象類的區(qū)別詳解

    這篇文章主要介紹了Java中接口和抽象類的區(qū)別詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-07-07
  • java 線程中start方法與run方法的區(qū)別詳細(xì)介紹

    java 線程中start方法與run方法的區(qū)別詳細(xì)介紹

    這篇文章主要介紹了java 線程中start方法與run方法的區(qū)別詳細(xì)介紹的相關(guān)資料,在java線程中調(diào)用start方法與run方法的區(qū)別在哪里? 這兩個(gè)問題是兩個(gè)非常流行的初學(xué)者級別的多線程面試問題,這里進(jìn)行詳細(xì)說明,需要的朋友可以參考下
    2016-11-11
  • Java基礎(chǔ)入門篇之邏輯控制練習(xí)題與猜數(shù)字游戲

    Java基礎(chǔ)入門篇之邏輯控制練習(xí)題與猜數(shù)字游戲

    猜數(shù)字游戲是一款經(jīng)典的游戲,該游戲說簡單也很簡單,說不簡單確實(shí)也很難,這篇文章主要給大家介紹了關(guān)于Java基礎(chǔ)入門篇之邏輯控制練習(xí)題與猜數(shù)字游戲的相關(guān)資料,需要的朋友可以參考下
    2023-06-06
  • SpringBoot的攔截器中依賴注入為null的解決方法

    SpringBoot的攔截器中依賴注入為null的解決方法

    這篇文章主要介紹了SpringBoot的攔截器中依賴注入為null的解決方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-06-06
  • Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(41)

    Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(41)

    下面小編就為大家?guī)硪黄狫ava基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧,希望可以幫到你
    2021-07-07
  • Java SpringBoot快速集成SpringBootAdmin管控臺監(jiān)控服務(wù)詳解

    Java SpringBoot快速集成SpringBootAdmin管控臺監(jiān)控服務(wù)詳解

    這篇文章主要介紹了如何基于springboot-admin管控臺監(jiān)控服務(wù),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2021-09-09
  • java實(shí)現(xiàn)任意矩陣Strassen算法

    java實(shí)現(xiàn)任意矩陣Strassen算法

    這篇文章主要介紹了java實(shí)現(xiàn)任意矩陣Strassen算法的相關(guān)資料,需要的朋友可以參考下
    2016-02-02
  • spring cloud oauth2 實(shí)現(xiàn)用戶認(rèn)證登錄的示例代碼

    spring cloud oauth2 實(shí)現(xiàn)用戶認(rèn)證登錄的示例代碼

    這篇文章主要介紹了spring cloud oauth2 實(shí)現(xiàn)用戶認(rèn)證登錄的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-10-10

最新評論