Java分布式IP限流和防止恶意IP攻击方案
前言
限流是分布式系统设计中经常提到的概念,在某些要求不严格的场景下,使用Guava RateLimiter就可以满足。但是Guava RateLimiter只能应用于单进程,多进程间协同控制便无能为力。本文介绍一种简单的处理方式,用于分布式环境下接口调用频次管控。
如何防止恶意IP攻击某些暴露的接口呢(比如某些场景下短信验证码服务)?本文介绍一种本地缓存和分布式缓存集成方式判断远程IP是否为恶意调用接口的IP。
分布式IP限流
思路是使用redis incr命令,完成一段时间内接口请求次数的统计,以此来完成限流相关逻辑。
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";
这里为啥时候用lua脚本来实现呢?因为要保证incr命令和expire命令的原子性操作。KEYS[1]代表自增key值, ARGV[1]代表过期时间,ARGV[2]代表最大频次,明白了这些参数的含义,整个lua脚本逻辑也就不言而喻了。
/**
* @param limitKey 限制Key值
* @param maxRate 最大速率
* @param expire Key过期时间
*/
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 是否超过了最大访问频次。缓存服务对象(cacheService)的eval方法参数分别是lua脚本、key list、value list。
unlimit方法其实就是执行redis decr操作,在某些业务场景可以回退访问频次统计。
防止恶意IP攻击
由于某些对外暴露的接口很容易被恶意用户攻击,必须做好防范措施。最近我就遇到了这么一种情况,我们一个快应用产品,短信验证码服务被恶意调用了。通过后台的日志发现,IP固定,接口调用时间间隔固定,明显是被人利用了。虽然我们针对每个手机号每天发送短信验证码的次数限制在5次以内。但是短信验证码服务每天这样被重复调用,会打扰用户并产生投诉。针对这种现象,简单的做了一个方案,可以自动识别恶意攻击的IP并加入黑名单。
思路是这样的,针对某些业务场景,约定在一段时间内同一个IP访问最大频次,如果超过了这个最大频次,那么就认为是非法IP。识别了非法IP后,把IP同时放入本地缓存和分布式缓存中。非法IP再次访问的时候,拦截器发现本地缓存(没有则去分布式缓存)有记录这个IP,直接返回异常状态,不会继续执行正常业务逻辑。
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对象才有get方法,如果本地缓存不存在key值, 会执行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) {
//分布式缓存中不存在这个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这个抽象类封装了guava本地缓存和redis分布式缓存操作,可以降低分布式缓存压力。
防止恶意IP攻击缓存服务
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 过期时间
*/
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);
}
}
}
没有错,IP_RATE_LUA 这个lua脚本和上面说的限流方案对应的lua脚本是一样的。
IPBlackCache继承了AbstractCombineCache,构造函数需要guava的本地Cache对象和redis分布式缓存服务ICacheService 对象。
ipAccess方法用来判断当前ip访问次数是否在一定时间内已经达到了最大访问频次。
removeIpAccess方法是直接移除当前ip访问频次统计的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() + " 本地缓存移除时间:" + curTime);
try {
cacheService.del(notification.getKey());
LOGGER.info(notification.getKey() + " 分布式缓存移除时间:" + 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分布式缓存服务ICacheService对象。
通过GuavaCacheBuilder构建guava本地Cache对象,指定初始容量(initialCapacity)、最大容量(maximumSize)、并发级别、key过期时间、key移除监听器。最终要的是CacheLoader这个参数,是干什么用的呢?如果GuavaCacheBuilder指定了CacheLoader对象,那么最终创建的guava本地Cache对象是LoadingCache类型(参考AbstractCombineCache类的get方法),LoadingCache对象的get方法首先从内存中获取key对应的value,如果内存中不存在这个key则调用CacheLoader对象的load方法加载key对应的value值,加载成功后放入内存中。
最后通过ICacheService对象和guava本地Cache对象创建IPBlackCache(防止恶意IP攻击缓存服务)对象。
拦截器里恶意IP校验
定义一个注解,标注在指定方法上,拦截器里会识别这个注解。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IPBlackLimit {
//统计时间内最大速率
int maxRate(); //频次统计时间
int duration(); //方法名称
String method() default StringUtils.EMPTY;
}
拦截器里加入ipAccess方法,校验远程IP是否为恶意攻击的IP。
/**
* @param method 需要校验的方法
* @param remoteAddr 远程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对应的值。利用remoteAddr构造cacheKey参数,通过IPBlackCache判断cacheKey是否存在。
如果是cacheKey存在的请求,判断黑名单IP限制是否已经到达有效期,如果已经超过有效期则清除本地缓存和分布式缓存的cacheKey,请求合法;如果没有超过有效期则请求非法。
否则是cacheKey不存在的请求,使用IPBlackCache对象的ipAccess方法统计一定时间内的访问频次,如果频次超过最大限制,表明是非法请求IP,需要往IPBlackCache对象写入“cacheKey=当前时间”。
总结
本文的两种方案都使用redis incr命令,如果不是特殊业务场景,redis的key要指定过期时间,严格来讲需要保证incr和expire两个命令的原子性,所以使用lua脚本方式。如果没有那么严格,完全可以先setex(设置key,value,过期时间),然后再incr(注:incr不会更新key的有效期)。本文的设计方案仅供参考,并不能应用于所有的业务场景。
Java分布式IP限流和防止恶意IP攻击方案的更多相关文章
- 基于AOP和Redis实现对接口调用情况的监控及IP限流
目录 需求描述 概要设计 代码实现 参考资料 需求描述 项目中有许多接口,现在我们需要实现一个功能对接口调用情况进行统计,主要功能如下: 需求一:实现对每个接口,每天的调用次数做记录: 需求二:如果某 ...
- nginx限流模块(防范DDOS攻击)
Nginx限流模式(防范DDOS攻击) nginx中俩个限流模块: 1.ngx_http_limit_req_module(按请求速率限流) 2.ngx_http_limit_conn_module( ...
- TCP通过滑动窗口和拥塞窗口实现限流,能抵御ddos攻击吗
tcp可以通过滑动窗口和拥塞算法实现流量控制,限制上行和下行的流量,但是却不能抵御ddos攻击. 限流只是限制访问流量的大小,是无法区分正常流量和异常攻击流量的. 限流可以控制本软件或者应用的流量大小 ...
- 【分布式架构】--- 基于Redis组件的特性,实现一个分布式限流
分布式---基于Redis进行接口IP限流 场景 为了防止我们的接口被人恶意访问,比如有人通过JMeter工具频繁访问我们的接口,导致接口响应变慢甚至崩溃,所以我们需要对一些特定的接口进行IP限流,即 ...
- 一个轻量级的基于RateLimiter的分布式限流实现
上篇文章(限流算法与Guava RateLimiter解析)对常用的限流算法及Google Guava基于令牌桶算法的实现RateLimiter进行了介绍.RateLimiter通过线程锁控制同步,只 ...
- 基于kubernetes的分布式限流
做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机 ...
- 基于令牌桶算法实现的SpringBoot分布式无锁限流插件
本文档不会是最新的,最新的请看Github! 1.简介 基于令牌桶算法和漏桶算法实现的纳秒级分布式无锁限流插件,完美嵌入SpringBoot.SpringCloud应用,支持接口限流.方法限流.系统限 ...
- 分布式环境下限流方案的实现redis RateLimiter Guava,Token Bucket, Leaky Bucket
业务背景介绍 对于web应用的限流,光看标题,似乎过于抽象,难以理解,那我们还是以具体的某一个应用场景来引入这个话题吧. 在日常生活中,我们肯定收到过不少不少这样的短信,“双11约吗?,千款….”,“ ...
- 从SpringBoot构建十万博文聊聊限流特技
前言 在开发十万博客系统的的过程中,前面主要分享了爬虫.缓存穿透以及文章阅读量计数等等.爬虫的目的就是解决十万+问题:缓存穿透是为了保护后端数据库查询服务:计数服务解决了接近真实阅读数以及数据库服务的 ...
随机推荐
- mingster.com
Good to Great: Why Some Companies Make the Leap... and Others Don'tby Jim Collinshttp://rcm.amazon.c ...
- 手机视频APP将关闭 生态梦成空的三星如何自救?
生态梦成空的三星如何自救?"> 三星如今的处境,只能用"屋漏偏逢连夜雨"来形容.继营收.利润.智能手机销量等大幅下滑之后,裁员也接踵而来,股价的下跌也自然在情理之中 ...
- 一个很实用的css技巧简析
我是小雨小雨,专注于更新有趣.实用内容的小伙,如果内容对大家有一点帮助,那么就请动动手指,给个关注.点赞支持一下吧. ^ - ^ 序言 前两天接到一个需求,其中包括一个有序的列表,我们今天就来看看这个 ...
- CSS——NO.2(CSS样式的基本知识)
*/ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...
- js数组冒泡排序、快速排序、插入排序
1.冒泡排序 //第一种 function bubblesort(ary){ for(var i=0;i<ary.length-1;i++){ for(var j=0;j<ary.leng ...
- 前端H5,点击选择图片控件,图片直接在页面上展示~
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- 多图文,详细介绍mysql各个集群方案
目录 多图文,详细介绍mysql各个集群方案 一,mysql原厂出品 二,mysql第三方优化 三,依托硬件配合 四,其它 多图文,详细介绍mysql各个集群方案 集群的好处 高可用性:故障检测及迁移 ...
- C++ 随笔练习
//例题:求Sn=a+aa+aaa+…+aa…aaa(有n个a)之值,其中a是一个数字,为2. 例如,n=5时=2+22+222+2222+22222,n由键盘输入.//题目来源:https://ww ...
- [POI2017]bzoj4726 Sadota?
题目描述 离线题库请 题目描述 某个公司有\(n\)个人, 上下级关系构成了一个有根树.其中有个人是叛徒(这个人不知道是谁).对于一个人, 如果他 下属(直接或者间接, 不包括他自己)中叛徒占的比例超 ...
- 使用express+shell在服务器上搭建一套简单的前端部署系统
前言 个人项目越来越多,部署需要频繁操作服务器,所以手动搭建一套简单的部署系统. 效果如图 其中包含 原生html+css+js项目,单页面react, vue, angular项目,实现了一键打包发 ...