├── src ├── main │ └── java │ │ └── com │ │ └── van │ │ └── limiter │ │ └── core │ │ ├── enums │ │ ├── CurrentLimiterType.java │ │ ├── LimitTimeType.java │ │ └── LimitType.java │ │ ├── annotation │ │ ├── EnableIpLimit.java │ │ └── IpLimit.java │ │ ├── constant │ │ └── IpLimitConstant.java │ │ ├── exception │ │ └── IpLimitException.java │ │ ├── aspect │ │ ├── RateLimitAspectConfig.java │ │ └── IpLimitAspect.java │ │ └── util │ │ ├── IpLimitUtils.java │ │ └── IpUtils.java └── test │ └── java │ └── com │ └── van │ └── limiter │ └── core │ └── util │ └── IpUtilsTest.java ├── .gitignore ├── README_zh.md ├── pom.xml ├── README.md └── LICENSE /src/main/java/com/van/limiter/core/enums/CurrentLimiterType.java: -------------------------------------------------------------------------------- 1 | package com.van.limiter.core.enums; 2 | 3 | /** 4 | * 限流器类型 5 | * @author van 6 | */ 7 | public enum CurrentLimiterType { 8 | /** 9 | * 滑动窗口 10 | */ 11 | SLIDING_WINDOW, 12 | /** 13 | * 令牌桶 14 | */ 15 | TOKEN_BUCKET; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/van/limiter/core/enums/LimitTimeType.java: -------------------------------------------------------------------------------- 1 | package com.van.limiter.core.enums; 2 | 3 | /** 4 | * 限流的时间单位类型 5 | * @author van 6 | */ 7 | public enum LimitTimeType { 8 | /** 9 | * 毫秒 10 | */ 11 | MILLISECOND, 12 | /** 13 | * 秒 14 | */ 15 | SECOND, 16 | /** 17 | * 分钟 18 | */ 19 | MINUTE; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/van/limiter/core/annotation/EnableIpLimit.java: -------------------------------------------------------------------------------- 1 | package com.van.limiter.core.annotation; 2 | 3 | import com.van.limiter.core.aspect.RateLimitAspectConfig; 4 | import org.springframework.context.annotation.Import; 5 | 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | /** 12 | * 用于自动注册,使用时将该注解添加到 application 类上即可 13 | * @author van 14 | */ 15 | @Target(ElementType.TYPE) 16 | @Retention(RetentionPolicy.RUNTIME) 17 | @Import(RateLimitAspectConfig.class) 18 | public @interface EnableIpLimit { 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/van/limiter/core/enums/LimitType.java: -------------------------------------------------------------------------------- 1 | package com.van.limiter.core.enums; 2 | 3 | /** 4 | * 限流规则 5 | * @author van 6 | */ 7 | public enum LimitType { 8 | /** 9 | * 走默认限流策略,不考虑黑白名单参数 10 | */ 11 | DEFAULT, 12 | /** 13 | * 只考虑白名单策略,非白名单的请求全部回绝 14 | */ 15 | WHITE_LIST, 16 | /** 17 | * 只考虑黑名单策略,非黑名单请求不做限流措施 18 | */ 19 | BLACK_LIST, 20 | /** 21 | * 在默认限流策略的基础上,白名单内的IP不做限流 22 | */ 23 | DEFAULT_WITH_WHITE_LIST, 24 | /** 25 | * 在默认限流策略的基础上,直接403黑名单 26 | */ 27 | DEFAULT_WITH_BLACK_LIST, 28 | /** 29 | * 在默认限流策略的基础上,直接403黑名单,再让白名单内的IP直接同行 30 | */ 31 | DEFAULT_WITH_WHITE_AND_BLACK_LIST 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | *.mvn 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | 33 | ### apm ### 34 | fs/ 35 | docker_lib/ 36 | log/* 37 | /docker/tar/ 38 | /docker/base/nginx/apm-web/ 39 | /docker/base/nginx/logs/access.log 40 | /docker/base/nginx/logs/error.log 41 | web_debug.log 42 | web_error.log 43 | web_info.log 44 | web_warn.log 45 | /jar/ 46 | -------------------------------------------------------------------------------- /src/main/java/com/van/limiter/core/constant/IpLimitConstant.java: -------------------------------------------------------------------------------- 1 | package com.van.limiter.core.constant; 2 | 3 | 4 | /** 5 | * 常量类 6 | * @author van 7 | */ 8 | public class IpLimitConstant { 9 | private IpLimitConstant() {} 10 | 11 | /** 12 | * 默认的通用组名 13 | */ 14 | public static final String COMMON_LIMIT_GROUP = "COMMON_GROUP"; 15 | 16 | /** 17 | * properties中配置Ip时的分隔符 18 | */ 19 | public static final String IP_PROPERTIES_SPLIT = ","; 20 | 21 | /** 22 | * IP模糊匹配时的分隔符 23 | */ 24 | public static final String IP_FUZZY_SPLIT = "*"; 25 | 26 | /** 27 | * IPV4分几段 28 | */ 29 | public static final Integer IPV4_SEGMENT_SIZE = 4; 30 | 31 | /** 32 | * 单*模糊匹配场景时,应当分成两端,应对规则设置为 172.*.1 这种场景 33 | */ 34 | public static final Integer IPV4_SPLIT_TWO_SIZE = 2; 35 | 36 | /** 37 | * 单*模糊匹配场景时,应当分成两端,应对规则设置为 172.* / *.1 这种场景 38 | */ 39 | public static final Integer IPV4_START_END_SPLIT = 1; 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/van/limiter/core/exception/IpLimitException.java: -------------------------------------------------------------------------------- 1 | package com.van.limiter.core.exception; 2 | 3 | 4 | import com.van.limiter.core.annotation.IpLimit; 5 | 6 | /** 7 | * 专用于Ip限流异常,可用于自定义捕获 8 | * @author van 9 | */ 10 | public class IpLimitException extends RuntimeException { 11 | 12 | private static final long serialVersionUID = 8669822979975640792L; 13 | 14 | /** 15 | * 限流异常时触发的Ip 16 | */ 17 | private final String requestIp; 18 | /** 19 | * 异常接口的GroupName 20 | */ 21 | private final String groupName; 22 | /** 23 | * 异常的IpLimit注释信息 24 | */ 25 | private final IpLimit ipLimitAnnotation; 26 | 27 | public IpLimitException(String message, String requestIp, String groupName, IpLimit ipLimitAnnotation) { 28 | super(message); 29 | this.requestIp = requestIp; 30 | this.groupName = groupName; 31 | this.ipLimitAnnotation = ipLimitAnnotation; 32 | } 33 | 34 | public String getRequestIp() { 35 | return requestIp; 36 | } 37 | 38 | public String getGroupName() { 39 | return groupName; 40 | } 41 | 42 | public IpLimit getIpLimitAnnotation() {return ipLimitAnnotation;} 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/van/limiter/core/aspect/RateLimitAspectConfig.java: -------------------------------------------------------------------------------- 1 | package com.van.limiter.core.aspect; 2 | 3 | import com.google.common.collect.Maps; 4 | import com.google.common.util.concurrent.RateLimiter; 5 | import com.van.limiter.core.annotation.EnableIpLimit; 6 | import com.van.limiter.core.util.IpLimitUtils; 7 | import org.springframework.context.annotation.Bean; 8 | 9 | import java.time.LocalDateTime; 10 | import java.util.Deque; 11 | import java.util.Map; 12 | import java.util.concurrent.ConcurrentLinkedDeque; 13 | 14 | /** 15 | * @author van 16 | */ 17 | public class RateLimitAspectConfig { 18 | private RateLimitAspectConfig () {} 19 | /** 20 | * 存储全局请求时间队列 21 | */ 22 | protected static final Deque GLOBAL_REQUEST_TIMESTAMP_QUEUE = new ConcurrentLinkedDeque<>(); 23 | 24 | /** 25 | * 令牌桶模式,[IP,[Group,RateLimit-令牌桶]] , 用以区分不同groupName之间的限流措施 26 | * 可以暂时不考虑 @Beta 问题,考虑项目进度先采用令牌桶方案,后续考虑切换滑动窗口限流方案 27 | */ 28 | @SuppressWarnings("ALL") 29 | protected static final Map> TOKEN_BUCKET_LIMITER_MAP = Maps.newConcurrentMap(); 30 | 31 | 32 | /** 33 | * 滑动窗口模式,[IP,[Group,RateLimit]], 用以区分不同groupName之间的限流措施 34 | */ 35 | protected static final Map>> WINDOW_TIMESTAMP_LIMITER_MAP = Maps.newConcurrentMap(); 36 | 37 | /** 38 | * 用于 {@link EnableIpLimit} 自动扫描 39 | * @return aspect 40 | */ 41 | @Bean 42 | public IpLimitAspect aspect() { 43 | return new IpLimitAspect(); 44 | } 45 | 46 | /** 47 | * 用于开放给用户一些内部功能 48 | * @return IpLimitUtils 49 | */ 50 | @Bean 51 | public IpLimitUtils utils() { 52 | return new IpLimitUtils(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/com/van/limiter/core/util/IpUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.van.limiter.core.util; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | /** 7 | * @author van 8 | */ 9 | class IpUtilsTest { 10 | 11 | @Test 12 | void ipFuzzyMatchTest() { 13 | String matchStr1 = "172.*.*.*"; 14 | String matchStr2 = "172.*"; 15 | String matchStr3 = "*"; 16 | String matchStr4 = "*.21"; 17 | String matchStr5 = "172.16.50.21"; 18 | 19 | String ip1 = "172.16.50.21"; 20 | String ip2 = "173.16.50.21"; 21 | String ip3 = "172.16.50.22"; 22 | String ip4 = "172.17.51.21"; 23 | 24 | Assertions.assertTrue(IpUtils.ipFuzzyMatch(matchStr1, ip1)); 25 | Assertions.assertTrue(IpUtils.ipFuzzyMatch(matchStr2, ip1)); 26 | Assertions.assertTrue(IpUtils.ipFuzzyMatch(matchStr3, ip1)); 27 | Assertions.assertTrue(IpUtils.ipFuzzyMatch(matchStr4, ip1)); 28 | Assertions.assertTrue(IpUtils.ipFuzzyMatch(matchStr5, ip1)); 29 | 30 | Assertions.assertFalse(IpUtils.ipFuzzyMatch(matchStr1, ip2)); 31 | 32 | Assertions.assertTrue(IpUtils.ipFuzzyMatch(matchStr2, ip1)); 33 | Assertions.assertFalse(IpUtils.ipFuzzyMatch(matchStr2, ip2)); 34 | 35 | Assertions.assertTrue(IpUtils.ipFuzzyMatch(matchStr3, ip1)); 36 | Assertions.assertTrue(IpUtils.ipFuzzyMatch(matchStr3, ip2)); 37 | Assertions.assertTrue(IpUtils.ipFuzzyMatch(matchStr3, ip3)); 38 | Assertions.assertTrue(IpUtils.ipFuzzyMatch(matchStr3, ip4)); 39 | 40 | Assertions.assertTrue(IpUtils.ipFuzzyMatch(matchStr4, ip1)); 41 | Assertions.assertTrue(IpUtils.ipFuzzyMatch(matchStr4, ip2)); 42 | Assertions.assertFalse(IpUtils.ipFuzzyMatch(matchStr4, ip3)); 43 | 44 | Assertions.assertTrue(IpUtils.ipFuzzyMatch(matchStr5, ip1)); 45 | Assertions.assertFalse(IpUtils.ipFuzzyMatch(matchStr5, ip2)); 46 | Assertions.assertFalse(IpUtils.ipFuzzyMatch(matchStr5, ip3)); 47 | Assertions.assertFalse(IpUtils.ipFuzzyMatch(matchStr5, ip4)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/van/limiter/core/annotation/IpLimit.java: -------------------------------------------------------------------------------- 1 | package com.van.limiter.core.annotation; 2 | 3 | import com.van.limiter.core.constant.IpLimitConstant; 4 | import com.van.limiter.core.enums.CurrentLimiterType; 5 | import com.van.limiter.core.enums.LimitTimeType; 6 | import com.van.limiter.core.enums.LimitType; 7 | 8 | import java.lang.annotation.*; 9 | 10 | /** 11 | * 限流具体注解 12 | * @author van 13 | */ 14 | @Inherited 15 | @Target({ElementType.METHOD, ElementType.TYPE}) 16 | @Retention(RetentionPolicy.RUNTIME) 17 | public @interface IpLimit { 18 | 19 | /** 20 | * 限流器类型,默认采用滑动窗口限流器,可配置为令牌桶模式 21 | * todo(van) 可用户自行实现限流器 22 | * @return 限流器类型 23 | */ 24 | CurrentLimiterType currentLimiter() default CurrentLimiterType.SLIDING_WINDOW; 25 | 26 | /** 27 | * 限流组名称,用以分组限流,注解提供默认,该属性可自行维护 28 | * 限流策略为IP下,对应分组分开进行限流统计,如果不需要分组则使用默认即可 29 | * @return 限流组名称 30 | */ 31 | String groupName() default IpLimitConstant.COMMON_LIMIT_GROUP; 32 | 33 | /** 34 | * 限流策略类型,策略类型同一Group下应当维护同一种 35 | * DEFAULT 走默认限流策略,不考虑黑白名单参数 36 | * LimitType.WHITE_LIST 只考虑白名单策略,非白名单的请求全部回绝 37 | * LimitType.BLACK_LIST 只考虑黑名单策略,非黑名单请求不做限流措施 38 | * LimitType.DEFAULT_WITH_WHITE_LIST 在默认限流策略的基础上,白名单内的IP不做限流 39 | * LimitType.DEFAULT_WITH_BLACK_LIST 在默认限流策略的基础上,直接403黑名单 40 | * LimitType.DEFAULT_WITH_WHITE_AND_BLACK_LIST 在默认限流策略的基础上,直接403黑名单,再让白名单内的IP直接通行 41 | * @return 限流策略类型 42 | */ 43 | LimitType limitType() default LimitType.DEFAULT; 44 | 45 | /** 46 | * 限流时间单位类型 47 | * @return 毫秒,秒,分钟 48 | */ 49 | LimitTimeType limitTimeType() default LimitTimeType.SECOND; 50 | 51 | /** 52 | * 限流的单位时间长度 53 | * @return 时间长度,单位由 {@link IpLimit#limitTimeType()} 决定 54 | */ 55 | long unitTime() default 1; 56 | 57 | /** 58 | * 限流单位时间长度内的最多次数 59 | * @return 最多次数 60 | */ 61 | double maxTimes() default 10; 62 | 63 | /** 64 | * 白名单,使用方式由 limitType() 确定,比如选择默认LimitType.DEFAULT时该参数配置无用 65 | * @return 白名单str 66 | */ 67 | String[] whiteList() default {"localhost", "127.0.0.1", "0:0:0:0:0:0:0:1"}; 68 | 69 | /** 70 | * 黑名单,使用方式由 limitType() 确定,比如选择默认LimitType.DEFAULT时该参数配置无用 71 | * @return 黑名单list 72 | */ 73 | String[] blackList() default {}; 74 | } -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | ~~~ 2 | author: van , ggfanwentao@gmail.com 3 | ~~~ 4 | English | [点击跳转](README.md) 5 | 6 | --- 7 | 8 | # Ip-Limiter: 轻量级注解式IP限流组件 9 | 10 | ## 项目简介 11 | 基于JVM缓存的轻量级、注解式IP限流组件,方便项目快速引用,满足多线程场景。 12 | 13 | ### 使用样例 14 | > 包含较为详细的演示使用代码 15 | 16 | 项目地址: https://github.com/DDAaTao/ip-limiter-example 17 | 项目国内地址: https://gitee.com/fanwentaomayun/ip-limiter-example 18 | 19 | **Ip-Limiter 具有以下特性:** 20 | - 基于注解使用,简单快捷,可添加到Controller类上,也可以添加到具体的API方法上 21 | - 业务入侵小,不用过多配置类,但可以支持多种场景配置 22 | - 实现组级别统一限流,即可满足单接口单组场景,又可满足多接口单组 23 | - 支持配置文件配置、外部动态配置(新增、删除)黑白名单 24 | 25 | ```properties 26 | # 配置文件中配置,需要注意分隔符为半角的',' 27 | my.white.ip.list=172.16.50.21,172.16.50.22,172.16.50.23 28 | ``` 29 | ``` 30 | // 代码中使用时 31 | @IpLimit(limitType = LimitType.WHITE_LIST, whiteList = "${my.white.ip.list}") 32 | // 或 33 | @IpLimit(limitType = LimitType.WHITE_LIST, whiteList = {"${my.white.ip.list}","172.16.50.35"}) 34 | ``` 35 | 36 | - 黑白名单IP规则实现多种模糊模式配置,支持IPv6 37 | - 172.\*.\*.1 38 | - 172.*.1 39 | - 172.* 40 | - *.21 41 | - \* 42 | 43 | **核心限流模式 - LimitType类** 44 | - DEFAULT - 走默认限流策略,不考虑黑白名单参数 45 | - WHITE_LIST - 只考虑白名单策略,非白名单的请求全部回绝 46 | - BLACK_LIST - 只考虑黑名单策略,非黑名单请求不做限流措施 47 | - DEFAULT_WITH_WHITE_LIST - 在默认限流策略的基础上,白名单内的IP不做限流 48 | - DEFAULT_WITH_BLACK_LIST - 在默认限流策略的基础上,直接403黑名单 49 | - DEFAULT_WITH_WHITE_AND_BLACK_LIST - 在默认限流策略的基础上,直接403黑名单,再让白名单内的IP直接同行 50 | 51 | ## 快速开始 52 | 53 | 1. 引入Ip-Limit依赖(已发布至Maven中央仓库) 54 | ```xml 55 | 56 | 57 | io.github.DDAaTao 58 | ip-limiter 59 | 1.0.3 60 | 61 | ``` 62 | 2. 将 @EnableIpLimit 添加到 webApplication 类上,或其他可以被 Spring 扫描到的类上 63 | 3. 将 @IpLimit 注解添加到想要做IP限流的方法(接口)上,根据需求动态调整参数 64 | 65 | > 如果项目中没有引入guava、spring-context包,则需要手动引入,否则会报java.lang.NoSuchMethodError异常 66 | > 67 | > 从1.0.1开始默认引入,如果项目中已有相关依赖,可以考虑通过exclusions去除掉 68 | 69 | ## 最佳实践 70 | ### 一、自定义限流异常处理机制 71 | ```Java 72 | /** 73 | * 默认情况下,当请求超出限流限制时,会打印日志并抛出 IpLimitException 异常 74 | * 用户可以通过统一异常拦截器捕获并自定义业务处理 75 | * 后续考虑增加回调或钩子方法 76 | * */ 77 | @Slf4j 78 | @ControllerAdvice 79 | public class BaseExceptionHandler { 80 | 81 | @ExceptionHandler(IpLimitException.class) 82 | @ResponseBody 83 | public RestApiResult resolveCommonException(IpLimitException e) { 84 | log.error("IpLimitException Intercept. Please try again later.. " + e.getMessage()); 85 | // 此处可以通过 e.getRequestIp() 和 e.getGroupName() 做一些限流回调业务处理 86 | return RestApiResult.fail("IpLimitException Intercept. Please try again later.. "); 87 | } 88 | 89 | } 90 | 91 | ``` 92 | ### 二、已存在鉴权方案时的接入方案 93 | 94 | SpringCloud 项目或者大部分项目一般都会有做自己的鉴权机制,比如Spring-Security。 95 | 这个时候如果有需要和外部对接的接口,有两种处理方法,一个是通过类似Oauth2之类的三方协议处理, 96 | 但是流程对接较为麻烦。 97 | 98 | 尤其是有些内网项目,本身已有较好的安全保证。此时就可以另外一种方式,也就是 **白名单** 来处理 99 | 也就是 LimitType.WHITE_LIST 100 | 101 | 或在白名单之上追加限流规则,保障系统的可用性,也就是 LimitType.DEFAULT_WITH_WHITE_LIST 102 | 103 | 104 | ### 三、动态配置黑白名单 105 | > 1.0.3 版本开始提供IpLimitUtils工具类,通过注入获取实例后可以实现动态配置黑白名单,该动态配置数据与注解中的配置取并集 106 | 107 | ***IpLimitUtils提供方法如下*** 108 | - putWhiteIpGroup - 可通过该方法动态配置新增白名单 109 | - removeWhiteIpGroup - 可通过该方法动态清空对应 group 的白名单配置 110 | - deleteWhiteIpGroupArrayStr - 可通过该方法动态去掉对应 group 中的某项 arrayStr 白名单 111 | - putBlackIpGroup - 可通过该方法动态配置新增黑名单 112 | - removeBlackIpGroup - 可通过该方法动态清空对应 group 的黑名单配置 113 | - deleteBlackIpGroupArrayStr - 可通过该方法动态去掉对应 group 中的某项 arrayStr 黑名单 114 | 115 | ***有了这些方法,就可以通过第三方(比如数据库)存储黑白名单数据,然后动态初始化、修改黑名单配置*** 116 | 117 | ## 异常记录 118 | 1. 暂时不支持Spring-6.x 119 | 120 | 121 | ## 更新日志 122 | > 加粗表示重要版本更新,删除线表示废弃版本,不建议使用 123 | - ~~1.0.1~~ 实现滑动窗口限流模式 124 | - 1.0.2 调整规范,添加样例项目链接 125 | - ___1.0.3___ 开放用户动态配置黑白名单 126 | 127 | 128 | 129 | 130 | 131 | ## Ip-Limit 计划实现功能 132 | - 用户自定义限流器 133 | - 全局限流、全局分IP限流 134 | - 添加限流监控,监控数据回调(目前可以通过@ExceptionHandler(IpLimitException.class)处理异常回调) 135 | - IP缓存统计数据可更换其他存储数据源,避免过多占用JVM缓存 136 | - 可将IP更换为指定字段(比如账号)限流 137 | - 更加灵活的异常处理机制 138 | - 支持Spring-Gateway -------------------------------------------------------------------------------- /src/main/java/com/van/limiter/core/util/IpLimitUtils.java: -------------------------------------------------------------------------------- 1 | package com.van.limiter.core.util; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.google.common.collect.Maps; 5 | import com.van.limiter.core.annotation.IpLimit; 6 | import com.van.limiter.core.constant.IpLimitConstant; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.core.env.Environment; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | /** 15 | * 通过注入获取实例后可以实现动态配置黑白名单 16 | * @author van 17 | */ 18 | @Component 19 | public class IpLimitUtils { 20 | 21 | /** 22 | * 动态配置白名单Map,[Group,[IpStr]] 23 | */ 24 | private static final Map> WHITE_IP_LIST = Maps.newHashMap(); 25 | 26 | /** 27 | * 动态配置黑名单Map,[Group,[IpStr]] 28 | */ 29 | private static final Map> BLACK_IP_LIST = Maps.newHashMap(); 30 | 31 | @Autowired 32 | private Environment environment; 33 | 34 | 35 | /** 36 | * 可通过该方法动态配置新增白名单 37 | * @param groupName 要配置的groupName 38 | * @param arrayStr 对应的IPStr,支持多种格式 39 | * @return success 40 | */ 41 | public boolean putWhiteIpGroup(String groupName, String arrayStr) { 42 | List arrayStrList = WHITE_IP_LIST.computeIfAbsent(groupName, k -> Lists.newArrayList()); 43 | return arrayStrList.add(arrayStr); 44 | } 45 | 46 | /** 47 | * 可通过该方法动态清空对应 group 的白名单配置 48 | * @param groupName groupName 49 | * @return 被清空的白名单数据 50 | */ 51 | public List removeWhiteIpGroup(String groupName) { 52 | return WHITE_IP_LIST.remove(groupName); 53 | } 54 | 55 | /** 56 | * 可通过该方法动态去掉对应 group 中的某项 arrayStr 白名单 57 | * @param groupName groupName 58 | * @param arrayStr arrayStr 59 | * @return 去掉后的 group 白名单情况 60 | */ 61 | public List deleteWhiteIpGroupArrayStr(String groupName, String arrayStr) { 62 | return WHITE_IP_LIST.computeIfPresent(groupName, (k, v) -> { 63 | v.remove(arrayStr); 64 | return v; 65 | }); 66 | } 67 | 68 | /** 69 | * 可通过该方法动态配置新增黑名单 70 | * @param groupName 要配置的groupName 71 | * @param arrayStr 对应的IPStr,支持多种格式 72 | * @return success 73 | */ 74 | public boolean putBlackIpGroup(String groupName, String arrayStr) { 75 | List arrayStrList = BLACK_IP_LIST.computeIfAbsent(groupName, k -> Lists.newArrayList()); 76 | return arrayStrList.add(arrayStr); 77 | } 78 | 79 | 80 | /** 81 | * 可通过该方法动态清空对应 group 的黑名单配置 82 | * @param groupName groupName 83 | * @return 被清空的黑名单数据 84 | */ 85 | public List removeBlackIpGroup(String groupName) { 86 | return BLACK_IP_LIST.remove(groupName); 87 | } 88 | 89 | /** 90 | * 可通过该方法动态去掉对应 group 中的某项 arrayStr 黑名单 91 | * @param groupName groupName 92 | * @param arrayStr arrayStr 93 | * @return 去掉后的 group 黑名单情况 94 | */ 95 | public List deleteBlackIpGroupArrayStr(String groupName, String arrayStr) { 96 | return BLACK_IP_LIST.computeIfPresent(groupName, (k, v) -> { 97 | v.remove(arrayStr); 98 | return v; 99 | }); 100 | } 101 | 102 | /** 103 | * 判断IP是否在白名单列表里 104 | * @param ipLimit ipLimit 105 | * @param ip ip 106 | * @return 是否存在 107 | */ 108 | public boolean ipInWhiteIpList(IpLimit ipLimit, String ip) { 109 | boolean annData = strInIpArray(ipLimit.whiteList(), ip); 110 | List configWhites = WHITE_IP_LIST.get(ipLimit.groupName()); 111 | if (configWhites != null && !configWhites.isEmpty()) { 112 | // 动态配置Map或注释中只要存在一个就算命中 113 | return annData || strInIpList(configWhites, ip); 114 | } 115 | return annData; 116 | } 117 | 118 | /** 119 | * 判断IP是否在黑名单列表里 120 | * @param ipLimit ipLimit 121 | * @param ip ip 122 | * @return 是否存在 123 | */ 124 | public boolean ipInBlackIpList(IpLimit ipLimit, String ip) { 125 | boolean annData = strInIpArray(ipLimit.blackList(), ip); 126 | List configBlacks = BLACK_IP_LIST.get(ipLimit.groupName()); 127 | if (configBlacks != null && !configBlacks.isEmpty()) { 128 | // 动态配置Map或注释中只要存在一个就算命中 129 | return annData || strInIpList(configBlacks, ip); 130 | } 131 | return annData; 132 | } 133 | 134 | /** 135 | * 判断对应字符串是否存在于数组内 136 | * @param array array 137 | * @param str 字符串 138 | * @return boolean 139 | */ 140 | public boolean strInIpArray(String[] array, String str) { 141 | for (String arrayStr : array) { 142 | if (ipMatch(arrayStr, str)) { 143 | return true; 144 | } 145 | } 146 | return false; 147 | } 148 | 149 | /** 150 | * 判断对应字符串是否存在于数组内 151 | * @param array array 152 | * @param str 字符串 153 | * @return boolean 154 | */ 155 | public boolean strInIpList(List array, String str) { 156 | for (String arrayStr : array) { 157 | if (ipMatch(arrayStr, str)) { 158 | return true; 159 | } 160 | } 161 | return false; 162 | } 163 | 164 | /** 165 | * 通过environment实现可通过properties配置对应的映射arrayStr 166 | * @param arrayStr 组合Str 167 | * @param ip ip 168 | * @return 是否包含 169 | */ 170 | private boolean ipMatch(String arrayStr, String ip) { 171 | arrayStr = environment.resolvePlaceholders(arrayStr); 172 | String[] split = arrayStr.split(IpLimitConstant.IP_PROPERTIES_SPLIT); 173 | for (String s : split) { 174 | if (IpUtils.ipFuzzyMatch(s.trim(), ip)) { 175 | return true; 176 | } 177 | } 178 | return false; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/main/java/com/van/limiter/core/util/IpUtils.java: -------------------------------------------------------------------------------- 1 | package com.van.limiter.core.util; 2 | 3 | import com.google.common.collect.Sets; 4 | import com.van.limiter.core.constant.IpLimitConstant; 5 | import org.springframework.util.StringUtils; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import java.net.Inet4Address; 9 | import java.net.InetAddress; 10 | import java.net.NetworkInterface; 11 | import java.net.SocketException; 12 | import java.util.Enumeration; 13 | import java.util.Set; 14 | import java.util.regex.Pattern; 15 | 16 | /** 17 | * IP 工具类 18 | * @author van 19 | */ 20 | public class IpUtils { 21 | private IpUtils() {} 22 | 23 | /** 24 | * 内网ip正则 25 | *
    26 | *
  • 127.0.0.1 或 localhost
  • 27 | *
  • 10.x.x.x
  • 28 | *
  • 172.16.x.x - 172.31.x.x
  • 29 | *
  • 192.168.x.x
  • 30 | *
31 | */ 32 | private static final Pattern LAN_PATTERN = Pattern.compile("^(127\\.0\\.0\\.1)|(localhost)" + 33 | "|(10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})" + 34 | "|(172\\.((1[6-9])|(2\\d)|(3[01]))\\.\\d{1,3}\\.\\d{1,3})" + 35 | "|(192\\.168\\.\\d{1,3}\\.\\d{1,3})$"); 36 | /** 37 | * localhost set 38 | */ 39 | private static final Set LOCAL_HOST_SET = Sets.newHashSet("0:0:0:0:0:0:0:1", "localhost"); 40 | 41 | /** 42 | * 判断ip是否为空,空返回true 43 | * 44 | * @param ip ip 45 | * @return bol 46 | */ 47 | public static boolean isEmptyIp(final String ip) { 48 | return (ip == null || ip.length() == 0 || ip.trim().equals("") || "unknown".equalsIgnoreCase(ip)); 49 | } 50 | 51 | 52 | /** 53 | * 判断ip是否不为空,不为空返回true 54 | * 55 | * @param ip ip 56 | * @return bol 57 | */ 58 | public static boolean isNotEmptyIp(final String ip) { 59 | return !isEmptyIp(ip); 60 | } 61 | 62 | /*** 63 | * 获取客户端ip地址(可以穿透代理) 64 | * @param request HttpServletRequest 65 | * @return ip 66 | */ 67 | public static String getIpAddress(HttpServletRequest request) { 68 | String ip = request.getHeader("X-Forwarded-For"); 69 | if (!StringUtils.isEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) { 70 | int index = ip.indexOf(','); 71 | if (index != -1) { 72 | return ip.substring(0, index); 73 | } else { 74 | return ip; 75 | } 76 | } 77 | ip = request.getHeader("X-Real-IP"); 78 | if (!StringUtils.isEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) { 79 | return ip; 80 | } 81 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 82 | ip = request.getHeader("Proxy-Client-IP"); 83 | } 84 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 85 | ip = request.getHeader("WL-Proxy-Client-IP"); 86 | } 87 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 88 | ip = request.getHeader("HTTP_CLIENT_IP"); 89 | } 90 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 91 | ip = request.getHeader("HTTP_X_FORWARDED_FOR"); 92 | } 93 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 94 | ip = request.getRemoteAddr(); 95 | } 96 | return ip; 97 | } 98 | 99 | /** 100 | * 获取本机的局域网ip地址,兼容Linux 101 | * 102 | * @return String 103 | * @throws SocketException SocketException 104 | */ 105 | public static String getLocalHostIP() throws SocketException { 106 | Enumeration allNetInterfaces = NetworkInterface.getNetworkInterfaces(); 107 | String localHostAddress = ""; 108 | while (allNetInterfaces.hasMoreElements()) { 109 | NetworkInterface networkInterface = allNetInterfaces.nextElement(); 110 | Enumeration address = networkInterface.getInetAddresses(); 111 | while (address.hasMoreElements()) { 112 | InetAddress inetAddress = address.nextElement(); 113 | if (inetAddress instanceof Inet4Address) { 114 | localHostAddress = inetAddress.getHostAddress(); 115 | } 116 | } 117 | } 118 | return localHostAddress; 119 | } 120 | 121 | /** 122 | * 判断是否为局域网ip 123 | * @param ip ip 124 | * @return boolean 125 | */ 126 | private static boolean isLAN(String ip) { 127 | ip = ip.trim(); 128 | return LAN_PATTERN.matcher(ip).matches() || LOCAL_HOST_SET.contains(ip.toLowerCase()); 129 | } 130 | 131 | /** 132 | * 是否是内网访问 133 | * @param request HttpServletRequest 134 | * @return boolean 135 | */ 136 | public static boolean isLanAccess(HttpServletRequest request) { 137 | String host = getRequestHost(request); 138 | return isLAN(host); 139 | } 140 | 141 | public static String getRequestHost(HttpServletRequest request) { 142 | String remoteHost = request.getHeader("host"); 143 | if (LOCAL_HOST_SET.contains(remoteHost)) { 144 | return "localhost"; 145 | } 146 | int colonIndex = remoteHost.indexOf(":"); 147 | return colonIndex > 0 ? remoteHost.substring(0, colonIndex) : remoteHost; 148 | } 149 | 150 | /** 151 | * 判断 ip 是否满足模糊匹配 matchIpPattern 152 | * @param matchIpPattern 模糊匹配规则,支持多种模式 eg. 172.*.21 ; 172.*.*.21; 153 | * @param ip 需要进行匹配的ip 154 | * @return isAccess 155 | */ 156 | public static boolean ipFuzzyMatch(String matchIpPattern, String ip) { 157 | if (!matchIpPattern.contains(IpLimitConstant.IP_FUZZY_SPLIT)) { 158 | return matchIpPattern.equals(ip); 159 | } 160 | 161 | if (matchIpPattern.equals(IpLimitConstant.IP_FUZZY_SPLIT)) { 162 | return true; 163 | } 164 | 165 | // 针对简易单星号场景 166 | String[] split = matchIpPattern.split("\\*"); 167 | if (split.length == IpLimitConstant.IPV4_START_END_SPLIT) { 168 | return matchIpPattern.startsWith(IpLimitConstant.IP_FUZZY_SPLIT) 169 | ? ip.endsWith(split[0]) 170 | : ip.startsWith(split[0]); 171 | } 172 | if (split.length == IpLimitConstant.IPV4_SPLIT_TWO_SIZE) { 173 | return ip.startsWith(split[0]) && ip.endsWith(split[1]); 174 | } 175 | 176 | // 不是一个星号的时候 177 | String[] matchStrSplit = matchIpPattern.split("\\."); 178 | String[] ipSplit = ip.split("\\."); 179 | for (int index = 0; index < IpLimitConstant.IPV4_SEGMENT_SIZE; index ++) { 180 | if (IpLimitConstant.IP_FUZZY_SPLIT.equals(matchStrSplit[index])) { 181 | continue; 182 | } 183 | if (!matchStrSplit[index].equals(ipSplit[index])) { 184 | return false; 185 | } 186 | } 187 | return true; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | io.github.DDAaTao 7 | ip-limiter 8 | 1.0.4 9 | jar 10 | 11 | ip-limiter 12 | Annotated stream limiting component 13 | https://github.com/DDAaTao/ip-limiter 14 | 15 | 16 | The Apache Software License, Version 2.0 17 | http://www.apache.org/licenses/LICENSE-2.0.txt 18 | 19 | 20 | 21 | 22 | van 23 | xxs 24 | ggfanwentao@gmail.com 25 | 26 | Project Manager 27 | Architect 28 | 29 | +8 30 | 31 | 32 | 33 | https://github.com/DDAaTao/ip-limiter.git 34 | scm:git:ssh://git@github.com:DDAaTao/ip-limiter.git 35 | https://github.com/DDAaTao/ip-limiter 36 | 37 | 38 | 39 | 40 | 8 41 | 8 42 | UTF-8 43 | 44 | 45 | 46 | 47 | com.google.guava 48 | guava 49 | 32.0.1-jre 50 | 51 | 52 | org.springframework 53 | spring-aop 54 | 5.1.14.RELEASE 55 | provided 56 | 57 | 58 | org.aspectj 59 | aspectjweaver 60 | 1.9.5 61 | 62 | 63 | javax.servlet 64 | javax.servlet-api 65 | 4.0.1 66 | 67 | 68 | org.springframework 69 | spring-web 70 | 5.1.14.RELEASE 71 | provided 72 | 73 | 74 | org.springframework 75 | spring-context 76 | 5.1.14.RELEASE 77 | 78 | 79 | 80 | org.junit.jupiter 81 | junit-jupiter 82 | 5.9.0 83 | test 84 | 85 | 86 | 87 | 88 | 89 | release 90 | 91 | true 92 | 93 | 94 | 95 | 96 | 97 | org.sonatype.plugins 98 | nexus-staging-maven-plugin 99 | 1.6.7 100 | true 101 | 102 | release 103 | https://s01.oss.sonatype.org/ 104 | true 105 | 106 | 107 | 108 | 109 | 110 | org.apache.maven.plugins 111 | maven-source-plugin 112 | 2.2.1 113 | 114 | 115 | attach-sources 116 | 117 | jar-no-fork 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | org.apache.maven.plugins 126 | maven-javadoc-plugin 127 | 2.9.1 128 | 129 | 130 | attach-javadocs 131 | 132 | jar 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | org.apache.maven.plugins 141 | maven-gpg-plugin 142 | 1.5 143 | 144 | 145 | sign-artifacts 146 | verify 147 | 148 | sign 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | release 160 | https://s01.oss.sonatype.org/content/repositories/snapshots 161 | 162 | 163 | release 164 | https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ~~~ 2 | author: van , ggfanwentao@gmail.com 3 | ~~~ 4 | 中文 | [点击跳转](README_zh.md) 5 | 6 | --- 7 | 8 | # Ip-Limiter: Lightweight Annotation-based IP Rate Limiting Component 9 | 10 | ## Project Introduction 11 | Ip-Limiter is a lightweight, annotation-based IP rate limiting component based on JVM cache, designed for easy integration into projects and to meet multi-threading scenarios. 12 | 13 | ### Usage Example 14 | > Contains detailed demonstration code 15 | 16 | Demo Project Repository: https://github.com/DDAaTao/ip-limiter-example \ 17 | Demo China Project Repository: https://gitee.com/fanwentaomayun/ip-limiter-example 18 | 19 | **Ip-Limit features include:** 20 | - Annotation-based usage, simple and convenient, can be added to Controller classes or specific API methods. 21 | - Minimal intrusion into business logic; no need for extensive configuration, yet supports various scenarios. 22 | - Provides group-level uniform rate limiting, serving both single interface and multiple interfaces within a group. 23 | - Supports configuration through property files and dynamic external configuration (addition and deletion) of blacklists and whitelists. 24 | 25 | ```properties 26 | # Configuration in a properties file, note that ',' is used as a delimiter 27 | my.white.ip.list=172.16.50.21,172.16.50.22,172.16.50.23 28 | ``` 29 | 30 | ``` 31 | // Usage in code 32 | @IpLimit(limitType = LimitType.WHITE_LIST, whiteList = "${my.white.ip.list}") 33 | // or 34 | @IpLimit(limitType = LimitType.WHITE_LIST, whiteList = {"${my.white.ip.list}","172.16.50.35"}) 35 | ``` 36 | 37 | - Black and white list IP rules support multiple fuzzy pattern configurations and IPv6: 38 | - 172.\*.\*.1 39 | - 172.*.1 40 | - 172.* 41 | - *.21 42 | - \* 43 | 44 | **Core rate limiting modes - LimitType class:** 45 | - DEFAULT - Follows the default rate limiting strategy, without considering black and white list parameters. 46 | - WHITE_LIST - Considers only the whitelist strategy; requests not in the whitelist are all rejected. 47 | - BLACK_LIST - Considers only the blacklist strategy; requests not in the blacklist are not rate-limited. 48 | - DEFAULT_WITH_WHITE_LIST - Builds upon the default rate limiting strategy, where IPs in the whitelist are not rate-limited. 49 | - DEFAULT_WITH_BLACK_LIST - Builds upon the default rate limiting strategy, directly returning a 403 error for IPs in the blacklist. 50 | - DEFAULT_WITH_WHITE_AND_BLACK_LIST - Builds upon the default rate limiting strategy, directly returning a 403 error for IPs in the blacklist and allowing IPs in the whitelist to proceed. 51 | 52 | ## Getting Started 53 | 54 | 1. Include the Ip-Limit dependency (available on Maven Central). 55 | ```xml 56 | 57 | 58 | io.github.DDAaTao 59 | ip-limiter 60 | 1.0.3 61 | 62 | ``` 63 | 2. Add @EnableIpLimit to the web application class or any class that can be scanned by Spring. 64 | 3. Add the @IpLimit annotation to the methods (endpoints) where you want to apply IP rate limiting, and adjust the parameters according to your needs. 65 | 66 | > If your project does not include guava and spring-context packages, you need to manually include them; otherwise, you may encounter a java.lang.NoSuchMethodError exception. 67 | > 68 | > Starting from version 1.0.1, these dependencies are included by default. If your project already has these dependencies, consider excluding them. 69 | 70 | ## Best Practices 71 | ### 1.Custom Rate Limit Exception Handling Mechanism 72 | ```Java 73 | /** 74 | * By default, when requests exceed rate limits, it logs an error and throws an IpLimitException. 75 | * Users can capture and customize the exception handling through a global exception handler. 76 | * Callbacks or hook methods may be added in the future. 77 | * */ 78 | @Slf4j 79 | @ControllerAdvice 80 | public class BaseExceptionHandler { 81 | 82 | @ExceptionHandler(IpLimitException.class) 83 | @ResponseBody 84 | public RestApiResult resolveCommonException(IpLimitException e) { 85 | log.error("IpLimitException Intercept. Please try again later.. " + e.getMessage()); 86 | // Here, you can perform rate limit callback processing using e.getRequestIp() and e.getGroupName() 87 | return RestApiResult.fail("IpLimitException Intercept. Please try again later.. "); 88 | } 89 | 90 | } 91 | ``` 92 | 93 | ### 2. Integration with Existing Authentication Solutions 94 | 95 | In SpringCloud projects or most projects, there is usually an existing authentication mechanism, such as Spring Security. In such cases, when there is a need to integrate with external interfaces, there are two approaches. One is to handle it through third-party protocols like OAuth2, but this can be a complex integration process. 96 | 97 | Especially in the case of internal network projects that already have robust security measures, another approach can be used, which is the whitelist method, denoted as LimitType.WHITE_LIST. 98 | 99 | Alternatively, you can add rate limiting rules on top of the whitelist to ensure system availability, using LimitType.DEFAULT_WITH_WHITE_LIST. 100 | 101 | ### 3. Dynamic Configuration of Black and White Lists 102 | > Starting from version 1.0.3, IpLimitUtils utility class is provided, which allows dynamic configuration of black and white lists. This dynamic configuration data is combined with the configuration specified in annotations. 103 | 104 | ***IpLimitUtils offers the following methods*** 105 | - `putWhiteIpGroup` - Allows dynamic addition of new entries to the whitelist. 106 | - `removeWhiteIpGroup` - Dynamically clears the whitelist for a specific group. 107 | - `deleteWhiteIpGroupArrayStr` - Allows dynamic removal of a specific arrayStr entry from the whitelist for a group. 108 | - `putBlackIpGroup` - Allows dynamic addition of new entries to the blacklist. 109 | - `removeBlackIpGroup` - Dynamically clears the blacklist for a specific group. 110 | - `deleteBlackIpGroupArrayStr` - Allows dynamic removal of a specific arrayStr entry from the blacklist for a group. 111 | 112 | ***With these methods, you can store black and white list data in third-party sources like databases and then dynamically initialize or modify blacklist configurations.*** 113 | 114 | ## Known Issues 115 | 1. Currently, it does not support Spring 6.x. 116 | 117 | ## Changelog 118 | > Bold indicates important version updates; strikethrough indicates deprecated versions, not recommended for use. 119 | 120 | - ~~1.0.1~~ Implemented sliding window rate limiting mode. 121 | - 1.0.2 Adjusted specifications and added a link to example projects. 122 | - ___1.0.3___ Introduced dynamic configuration of black and white lists for user customization. 123 | 124 | 125 | ## Ip-Limit Planned Features 126 | - User-customized rate limiters. 127 | - Global rate limiting and per-IP rate limiting. 128 | - Adding rate limiting monitoring and callback for monitoring data (currently handled via @ExceptionHandler(IpLimitException.class)). 129 | - IP cache statistics data can be stored in other data sources to avoid excessive JVM cache usage. 130 | - Support for using a specified field (e.g., account) for rate limiting. 131 | - More flexible exception handling mechanisms. 132 | - Support Spring-Gateway 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /src/main/java/com/van/limiter/core/aspect/IpLimitAspect.java: -------------------------------------------------------------------------------- 1 | package com.van.limiter.core.aspect; 2 | 3 | import com.google.common.collect.Maps; 4 | import com.google.common.util.concurrent.RateLimiter; 5 | import com.van.limiter.core.annotation.IpLimit; 6 | import com.van.limiter.core.exception.IpLimitException; 7 | import com.van.limiter.core.util.IpLimitUtils; 8 | import com.van.limiter.core.util.IpUtils; 9 | import org.aspectj.lang.ProceedingJoinPoint; 10 | import org.aspectj.lang.annotation.Around; 11 | import org.aspectj.lang.annotation.Aspect; 12 | import org.aspectj.lang.annotation.Pointcut; 13 | import org.aspectj.lang.reflect.MethodSignature; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.web.context.request.RequestContextHolder; 17 | import org.springframework.web.context.request.ServletRequestAttributes; 18 | 19 | import javax.servlet.http.HttpServletRequest; 20 | import java.time.LocalDateTime; 21 | import java.time.temporal.ChronoUnit; 22 | import java.time.temporal.TemporalUnit; 23 | import java.util.Deque; 24 | import java.util.Map; 25 | import java.util.concurrent.ConcurrentLinkedDeque; 26 | 27 | /** 28 | * @author van 29 | */ 30 | @Aspect 31 | @Component 32 | public class IpLimitAspect { 33 | 34 | @Autowired 35 | private IpLimitUtils ipLimitUtils; 36 | 37 | @Pointcut("@within(com.van.limiter.core.annotation.IpLimit) || @annotation(com.van.limiter.core.annotation.IpLimit)") 38 | private void pointMethod() { 39 | } 40 | 41 | @Around("pointMethod()") 42 | public Object around(ProceedingJoinPoint joinPoint) throws Throwable { 43 | ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 44 | assert attributes != null; 45 | 46 | IpLimit ipLimitAnnotation = null; 47 | if (joinPoint.getSignature() instanceof MethodSignature) { 48 | MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); 49 | ipLimitAnnotation = methodSignature.getMethod().getAnnotation(IpLimit.class); 50 | if (ipLimitAnnotation == null) { 51 | ipLimitAnnotation = methodSignature.getMethod().getDeclaringClass().getAnnotation(IpLimit.class); 52 | } 53 | } 54 | assert ipLimitAnnotation != null; 55 | 56 | HttpServletRequest request = attributes.getRequest(); 57 | String requestHost = IpUtils.getIpAddress(request); 58 | double permitsPerSecond = computePermitsPerSecond(ipLimitAnnotation); 59 | switch (ipLimitAnnotation.limitType()) { 60 | case DEFAULT: 61 | currentLimiterSwitch(ipLimitAnnotation, requestHost, permitsPerSecond); 62 | break; 63 | case WHITE_LIST: 64 | // 如果是白名单内的,则不再进行校验 65 | if (Boolean.TRUE.equals(ipLimitUtils.ipInWhiteIpList(ipLimitAnnotation, requestHost))) { 66 | return joinPoint.proceed(); 67 | } 68 | ipLimitError(ipLimitAnnotation, requestHost); 69 | break; 70 | case BLACK_LIST: 71 | // 如果存在于黑名单,则抛出异常 72 | if (Boolean.TRUE.equals(ipLimitUtils.ipInBlackIpList(ipLimitAnnotation, requestHost))) { 73 | ipLimitError(ipLimitAnnotation, requestHost); 74 | } 75 | return joinPoint.proceed(); 76 | case DEFAULT_WITH_WHITE_LIST: 77 | if (Boolean.TRUE.equals(ipLimitUtils.ipInWhiteIpList(ipLimitAnnotation, requestHost))) { 78 | return joinPoint.proceed(); 79 | } 80 | currentLimiterSwitch(ipLimitAnnotation, requestHost, permitsPerSecond); 81 | break; 82 | case DEFAULT_WITH_BLACK_LIST: 83 | if (Boolean.TRUE.equals(ipLimitUtils.ipInBlackIpList(ipLimitAnnotation, requestHost))) { 84 | ipLimitError(ipLimitAnnotation, requestHost); 85 | } 86 | currentLimiterSwitch(ipLimitAnnotation, requestHost, permitsPerSecond); 87 | break; 88 | case DEFAULT_WITH_WHITE_AND_BLACK_LIST: 89 | if (Boolean.TRUE.equals(ipLimitUtils.ipInBlackIpList(ipLimitAnnotation, requestHost))) { 90 | ipLimitError(ipLimitAnnotation, requestHost); 91 | } 92 | if (Boolean.TRUE.equals(ipLimitUtils.ipInWhiteIpList(ipLimitAnnotation, requestHost))) { 93 | return joinPoint.proceed(); 94 | } 95 | currentLimiterSwitch(ipLimitAnnotation, requestHost, permitsPerSecond); 96 | break; 97 | default:break; 98 | } 99 | return joinPoint.proceed(); 100 | } 101 | 102 | /** 103 | * 用于统一异常处理 104 | * @param ipLimitAnnotation ipLimitAnnotation 105 | * @param requestHost 请求方IP 106 | */ 107 | private void ipLimitError(IpLimit ipLimitAnnotation, String requestHost) { 108 | throw new IpLimitException(String.format("Ip limiter warning ! IP: %s, GroupName: %s", requestHost, ipLimitAnnotation.groupName()), 109 | requestHost, ipLimitAnnotation.groupName(), ipLimitAnnotation); 110 | } 111 | 112 | /** 113 | * 限流器选择入口方法 114 | * @param ipLimitAnnotation 用于获取分组等信息数据 115 | * @param requestHost 请求方IP 116 | * @param permitsPerSecond 计算后的每秒允许请求数量 117 | */ 118 | private void currentLimiterSwitch(IpLimit ipLimitAnnotation, String requestHost, double permitsPerSecond) { 119 | switch (ipLimitAnnotation.currentLimiter()) { 120 | case TOKEN_BUCKET: 121 | tokenBucketLimitMethod(ipLimitAnnotation, requestHost, permitsPerSecond); 122 | break; 123 | case SLIDING_WINDOW: 124 | windowLimitMethod(ipLimitAnnotation, requestHost); 125 | break; 126 | default:break; 127 | } 128 | } 129 | 130 | /** 131 | * 令牌桶限流核心逻辑 132 | * @param ipLimitAnnotation 用于获取分组等信息数据 133 | * @param requestHost 请求方IP 134 | * @param permitsPerSecond 计算后的每秒允许请求数量 135 | */ 136 | @SuppressWarnings("ALL") 137 | private void tokenBucketLimitMethod(IpLimit ipLimitAnnotation, String requestHost, double permitsPerSecond) { 138 | // 取并判断是否已记录该IP的限流Map 139 | Map rateLimiterMap = RateLimitAspectConfig.TOKEN_BUCKET_LIMITER_MAP.computeIfAbsent(requestHost, k -> { 140 | RateLimiter rateLimiter = RateLimiter.create(permitsPerSecond); 141 | Map stringRateLimiterMap = Maps.newConcurrentMap(); 142 | stringRateLimiterMap.put(ipLimitAnnotation.groupName(), rateLimiter); 143 | return stringRateLimiterMap; 144 | }); 145 | // 取并判断是否已记录该IP的对应Group限流情况 146 | RateLimiter rateLimiter = rateLimiterMap.computeIfAbsent(ipLimitAnnotation.groupName(), k -> { 147 | RateLimiter thisRateLimiter = RateLimiter.create(permitsPerSecond); 148 | rateLimiterMap.put(ipLimitAnnotation.groupName(), thisRateLimiter); 149 | return thisRateLimiter; 150 | }); 151 | 152 | // 判断是否可以获取令牌成功,否则报异常 153 | if (Boolean.FALSE.equals(rateLimiter.tryAcquire())) { 154 | ipLimitError(ipLimitAnnotation, requestHost); 155 | } 156 | } 157 | 158 | /** 159 | * 滑动窗口限流核心逻辑 160 | * @param ipLimitAnnotation 用于获取分组等信息数据 161 | * @param requestHost 请求方IP 162 | */ 163 | private void windowLimitMethod(IpLimit ipLimitAnnotation, String requestHost) { 164 | Map> stringDequeMap = RateLimitAspectConfig.WINDOW_TIMESTAMP_LIMITER_MAP 165 | .computeIfAbsent(requestHost, k -> { 166 | Map> dequeMap = Maps.newConcurrentMap(); 167 | dequeMap.put(ipLimitAnnotation.groupName(), new ConcurrentLinkedDeque<>()); 168 | return dequeMap; 169 | }); 170 | 171 | // 获取滑动窗口的时间窗时间类型,默认为毫秒 172 | TemporalUnit temporalUnit; 173 | switch (ipLimitAnnotation.limitTimeType()) { 174 | case SECOND: 175 | temporalUnit = ChronoUnit.SECONDS; 176 | break; 177 | case MINUTE: 178 | temporalUnit = ChronoUnit.MINUTES; 179 | break; 180 | default:temporalUnit = ChronoUnit.MILLIS; 181 | } 182 | 183 | // 丢弃超出滑动窗口的失效数据 184 | Deque dateTimeDeque = stringDequeMap.get(ipLimitAnnotation.groupName()); 185 | while (!dateTimeDeque.isEmpty() && LocalDateTime.now().minus(ipLimitAnnotation.unitTime(), temporalUnit).isAfter(dateTimeDeque.peekFirst())) { 186 | dateTimeDeque.pollFirst(); 187 | } 188 | 189 | // 超出最大请求次数报出异常 190 | if (dateTimeDeque.size() > ipLimitAnnotation.maxTimes()) { 191 | ipLimitError(ipLimitAnnotation, requestHost); 192 | } 193 | 194 | // 加入当前请求 195 | dateTimeDeque.push(LocalDateTime.now()); 196 | } 197 | 198 | 199 | /** 200 | * 将不同限流时间单位转化为秒级限流措施 201 | * @param ipLimit ipLimit 202 | * @return 每秒允许通过请求 203 | */ 204 | private double computePermitsPerSecond(IpLimit ipLimit) { 205 | switch (ipLimit.limitTimeType()) { 206 | case MILLISECOND: 207 | return ipLimit.maxTimes() * 1000 / ipLimit.unitTime() ; 208 | case SECOND: 209 | return ipLimit.maxTimes() / ipLimit.unitTime(); 210 | case MINUTE: 211 | return ipLimit.maxTimes() / (ipLimit.unitTime() * 60); 212 | default: 213 | return ipLimit.maxTimes(); 214 | } 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------