From 51c47751f4ef92bb111619ee9ceb7c3ce4e2dba4 Mon Sep 17 00:00:00 2001 From: Charles7c Date: Fri, 28 Jun 2024 23:34:51 +0800 Subject: [PATCH] =?UTF-8?q?refactor(security/limiter):=20=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E9=99=90=E6=B5=81=E5=99=A8=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E9=99=90=E6=B5=81=E5=99=A8=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../autoconfigure/RedissonProperties.java | 2 +- .../core/constant/StringConstants.java | 10 + .../continew-starter-security-limiter/pom.xml | 37 ++- .../limiter/annotation/RateLimiter.java | 25 +- .../limiter/annotation/RateLimiters.java | 5 +- .../limiter/aop/RateLimiterAspect.java | 265 ------------------ .../RateLimiterAutoConfiguration.java | 28 +- .../autoconfigure/RateLimiterProperties.java | 33 +-- .../core/DefaultRateLimiterNameGenerator.java | 109 +++++++ .../limiter/core/RateLimiterAspect.java | 200 +++++++++++++ .../core/RateLimiterNameGenerator.java | 39 +++ .../security/limiter/enums/LimitType.java | 4 +- .../exception/RateLimiterException.java | 5 + continew-starter-security/pom.xml | 7 - 14 files changed, 429 insertions(+), 340 deletions(-) delete mode 100644 continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/aop/RateLimiterAspect.java create mode 100644 continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/core/DefaultRateLimiterNameGenerator.java create mode 100644 continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/core/RateLimiterAspect.java create mode 100644 continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/core/RateLimiterNameGenerator.java diff --git a/continew-starter-cache/continew-starter-cache-redisson/src/main/java/top/continew/starter/cache/redisson/autoconfigure/RedissonProperties.java b/continew-starter-cache/continew-starter-cache-redisson/src/main/java/top/continew/starter/cache/redisson/autoconfigure/RedissonProperties.java index 697f5fb3..2f68e611 100644 --- a/continew-starter-cache/continew-starter-cache-redisson/src/main/java/top/continew/starter/cache/redisson/autoconfigure/RedissonProperties.java +++ b/continew-starter-cache/continew-starter-cache-redisson/src/main/java/top/continew/starter/cache/redisson/autoconfigure/RedissonProperties.java @@ -28,7 +28,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; * @author Charles7c * @since 1.0.0 */ -@ConfigurationProperties(prefix = "spring.data.redisson") +@ConfigurationProperties("spring.data.redisson") public class RedissonProperties { /** diff --git a/continew-starter-core/src/main/java/top/continew/starter/core/constant/StringConstants.java b/continew-starter-core/src/main/java/top/continew/starter/core/constant/StringConstants.java index df63320c..ff304c7d 100644 --- a/continew-starter-core/src/main/java/top/continew/starter/core/constant/StringConstants.java +++ b/continew-starter-core/src/main/java/top/continew/starter/core/constant/StringConstants.java @@ -262,6 +262,16 @@ public class StringConstants { */ public static final String CHINESE_COMMA = ","; + /** + * 圆括号(左) {@code "("} + */ + public static final String ROUND_BRACKET_START = "("; + + /** + * 圆括号(右) {@code ")"} + */ + public static final String ROUND_BRACKET_END = ")"; + /** * 路径模式 */ diff --git a/continew-starter-security/continew-starter-security-limiter/pom.xml b/continew-starter-security/continew-starter-security-limiter/pom.xml index 68c3c5ff..20345801 100644 --- a/continew-starter-security/continew-starter-security-limiter/pom.xml +++ b/continew-starter-security/continew-starter-security-limiter/pom.xml @@ -8,32 +8,27 @@ continew-starter-security-limiter - ContiNew Starter 安全模块 - 限流模块 + ContiNew Starter 安全模块 - 限流 - - org.redisson - redisson-spring-boot-starter - - - - org.aspectj - aspectjrt - 1.9.4 - - - org.aspectj - aspectjweaver - 1.9.4 - - - - cn.hutool - hutool-all + org.springframework.boot + spring-boot-starter-aop - + + + cn.hutool + hutool-crypto + + + + + top.continew + continew-starter-web + + + top.continew continew-starter-cache-redisson diff --git a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/annotation/RateLimiter.java b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/annotation/RateLimiter.java index 41fc0845..060552e4 100644 --- a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/annotation/RateLimiter.java +++ b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/annotation/RateLimiter.java @@ -33,40 +33,37 @@ import java.lang.annotation.*; public @interface RateLimiter { /** - * LimitType 限流模式 - * DEFAULT 全局限流 - * IP IP限流 - * CLUSTER 实例限流 + * 类型 */ - LimitType limitType() default LimitType.DEFAULT; + LimitType type() default LimitType.DEFAULT; /** - * 缓存实例名称 + * 名称 */ String name() default ""; /** - * 限流key 支持 Spring EL 表达式 + * 键(支持 Spring EL 表达式) */ String key() default ""; /** - * 单位时间产生的令牌数 + * 速率(指定时间间隔产生的令牌数) */ int rate() default Integer.MAX_VALUE; /** - * 限流时间 + * 速率间隔(时间间隔) */ - int rateInterval() default 0; + int interval() default 0; /** - * 时间单位,默认毫秒 + * 速率间隔时间单位(默认:毫秒) */ - RateIntervalUnit timeUnit() default RateIntervalUnit.MILLISECONDS; + RateIntervalUnit unit() default RateIntervalUnit.MILLISECONDS; /** - * 拒绝请求时的提示信息 + * 提示信息 */ - String message() default "您操作过于频繁,请稍后再试!"; + String message() default "操作过于频繁,请稍后再试"; } \ No newline at end of file diff --git a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/annotation/RateLimiters.java b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/annotation/RateLimiters.java index a8a1c71c..569ce473 100644 --- a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/annotation/RateLimiters.java +++ b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/annotation/RateLimiters.java @@ -19,7 +19,7 @@ package top.continew.starter.security.limiter.annotation; import java.lang.annotation.*; /** - * 限流组 + * 限流组注解 * * @author KAI * @since 2.2.0 @@ -28,8 +28,9 @@ import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RateLimiters { + /** - * 用于管理多个 RateLimiter + * 限流组 */ RateLimiter[] value(); } diff --git a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/aop/RateLimiterAspect.java b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/aop/RateLimiterAspect.java deleted file mode 100644 index aa9b87fc..00000000 --- a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/aop/RateLimiterAspect.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. - *

- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.gnu.org/licenses/lgpl.html - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package top.continew.starter.security.limiter.aop; - -import cn.hutool.crypto.SecureUtil; -import cn.hutool.extra.servlet.JakartaServletUtil; -import cn.hutool.extra.spring.SpringUtil; -import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Pointcut; -import org.aspectj.lang.reflect.MethodSignature; -import org.redisson.api.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.expression.BeanFactoryResolver; -import org.springframework.context.expression.MethodBasedEvaluationContext; -import org.springframework.core.DefaultParameterNameDiscoverer; -import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.expression.Expression; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.ParserContext; -import org.springframework.expression.common.TemplateParserContext; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; -import top.continew.starter.security.limiter.annotation.RateLimiter; -import top.continew.starter.security.limiter.annotation.RateLimiters; -import top.continew.starter.security.limiter.autoconfigure.RateLimiterProperties; -import top.continew.starter.security.limiter.enums.LimitType; -import top.continew.starter.security.limiter.exception.RateLimiterException; -import top.continew.starter.web.util.ServletUtils; - -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 限流 AOP 拦截器 - * - * @author KAI - * @since 2.2.0 - */ -@Aspect -@Component -public class RateLimiterAspect { - - private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class); - - private static final ConcurrentHashMap rateLimiterCache = new ConcurrentHashMap<>(); - - private static final RedissonClient CLIENT = SpringUtil.getBean(RedissonClient.class); - - private final ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer(); - private final ExpressionParser parser = new SpelExpressionParser(); - private final ParserContext parserContext = new TemplateParserContext(); - - @Autowired - private RateLimiterProperties rateLimiterProperties; - - /** - * 单个限流注解切点 - */ - @Pointcut("@annotation(top.continew.starter.security.limiter.annotation.RateLimiter)") - public void rateLimiterSinglePointCut() { - } - - /** - * 多个限流注解切点 - */ - @Pointcut("@annotation(top.continew.starter.security.limiter.annotation.RateLimiters)") - public void rateLimiterBatchPointCut() { - } - - /** - * 环绕通知,处理单个限流注解 - * - * @param joinPoint 切点 - * @param rateLimiter 限流注解 - * @return 返回目标方法的执行结果 - * @throws Throwable 异常 - */ - @Around("@annotation(rateLimiter)") - public Object aroundSingle(ProceedingJoinPoint joinPoint, RateLimiter rateLimiter) throws Throwable { - // 未开启限流功能,直接执行目标方法 - if (!rateLimiterProperties.isEnabled()) { - return joinPoint.proceed(); - } - if (isRateLimited(joinPoint, rateLimiter)) { - throw new RateLimiterException(rateLimiter.message()); - } - return joinPoint.proceed(); - } - - /** - * 环绕通知,处理多个限流注解 - * - * @param joinPoint 切点 - * @param rateLimiters 多个限流注解 - * @return 返回目标方法的执行结果 - * @throws Throwable 异常 - */ - @Around("@annotation(rateLimiters)") - public Object aroundBatch(ProceedingJoinPoint joinPoint, RateLimiters rateLimiters) throws Throwable { - // 未开启限流功能,直接执行目标方法 - if (!rateLimiterProperties.isEnabled()) { - return joinPoint.proceed(); - } - for (RateLimiter rateLimiter : rateLimiters.value()) { - if (isRateLimited(joinPoint, rateLimiter)) { - throw new RateLimiterException(rateLimiter.message()); - } - } - return joinPoint.proceed(); - } - - /** - * 执行限流逻辑 - * - * @param joinPoint 切点 - * @param rateLimiter 限流注解 - * @throws Throwable 异常 - */ - private boolean isRateLimited(ProceedingJoinPoint joinPoint, RateLimiter rateLimiter) throws Throwable { - try { - // 生成限流 Key - String redisKey = generateRedisKey(rateLimiter, joinPoint); - String encipherKey = SecureUtil.md5(redisKey); - // 确定限流类型 - RateType rateType = rateLimiter.limitType() == LimitType.CLUSTER ? RateType.PER_CLIENT : RateType.OVERALL; - - // 获取redisson限流实例 - RRateLimiter rRateLimiter = getRateLimiter(encipherKey); - RateIntervalUnit rateIntervalUnit = rateLimiter.timeUnit(); - int rateInterval = rateLimiter.rateInterval(); - int rate = rateLimiter.rate(); - // 判断是否需要更新限流器 - if (shouldUpdateRateLimiter(rRateLimiter, rateType, rate, rateInterval, rateIntervalUnit)) { - // 更新限流器 - rRateLimiter.setRate(rateType, rate, rateInterval, rateIntervalUnit); - } - // 尝试获取令牌 - return !rRateLimiter.tryAcquire(); - } catch (RateLimiterException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException("服务器限流异常,请稍候再试", e); - } - } - - /** - * 获取 Redisson RateLimiter 实例 - * - * @param key 限流器的 Key - * @return RateLimiter 实例 - */ - private RRateLimiter getRateLimiter(String key) { - RRateLimiter rRateLimiter = rateLimiterCache.get(key); - if (rRateLimiter == null) { - // 直接创建 RateLimiter 实例 - rRateLimiter = CLIENT.getRateLimiter(key); - rateLimiterCache.put(key, rRateLimiter); - } - return rRateLimiter; - } - - /** - * 判断是否需要更新限流器配置 - * - * @param rRateLimiter 现有的限流器 - * @param rateType 限流类型(OVERALL:全局限流;PER_CLIENT:单机限流) - * @param rate 速率(指定时间间隔产生的令牌数) - * @param rateInterval 速率间隔 - * @param rateIntervalUnit 时间单位 - * @return 是否需要更新配置 - */ - private boolean shouldUpdateRateLimiter(RRateLimiter rRateLimiter, - RateType rateType, - long rate, - long rateInterval, - RateIntervalUnit rateIntervalUnit) { - - RateLimiterConfig config = rRateLimiter.getConfig(); - return !Objects.equals(config.getRateType(), rateType) || !Objects.equals(config.getRate(), rate) || !Objects - .equals(config.getRateInterval(), rateIntervalUnit.toMillis(rateInterval)); - } - - /** - * 获取限流Key - * - * @param rateLimiter RateLimiter实例 - * @param point 切点 - * @return 限流Key - */ - private String generateRedisKey(RateLimiter rateLimiter, JoinPoint point) { - // 获取限流器配置的 key - String key = rateLimiter.key(); - // 如果 key 不为空,则解析表达式并获取最终的 key - key = Optional.ofNullable(key).map(k -> { - // 获取方法签名 - MethodSignature signature = (MethodSignature)point.getSignature(); - // 获取方法参数 - Object[] args = point.getArgs(); - // 创建表达式上下文 - MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(null, signature - .getMethod(), args, discoverer); - // 设置 Bean 解析器 - context.setBeanResolver(new BeanFactoryResolver(SpringUtil.getBeanFactory())); - // 解析表达式 - Expression expression; - if (StringUtils.startsWithIgnoreCase(k, parserContext.getExpressionPrefix()) && StringUtils - .endsWithIgnoreCase(k, parserContext.getExpressionSuffix())) { - expression = parser.parseExpression(k, parserContext); - } else { - expression = parser.parseExpression(k); - } - // 获取表达式结果 - return expression.getValue(context, String.class); - }).orElse(key); - - // 拼接最终的 key - StringBuilder redisKey = new StringBuilder(rateLimiterProperties.getLimiterKey()).append(ServletUtils - .getRequest() - .getRequestURI()).append(":"); - //如果缓存name 不为空 则拼接上去 - String name = rateLimiter.name(); - if (StringUtils.hasText(name)) { - redisKey.append(name); - if (!name.endsWith(":")) { - redisKey.append(":"); - } - } - // 根据限流类型添加不同的信息 - switch (rateLimiter.limitType()) { - case IP: - // 获取请求 IP - redisKey.append(JakartaServletUtil.getClientIP(ServletUtils.getRequest())).append(":"); - break; - case CLUSTER: - // 获取客户端实例 ID - redisKey.append(CLIENT.getId()).append(":"); - break; - default: - break; - } - // 添加解析后的 key - return redisKey.append(key).toString(); - } -} diff --git a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/autoconfigure/RateLimiterAutoConfiguration.java b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/autoconfigure/RateLimiterAutoConfiguration.java index a1dc75e6..87104ca9 100644 --- a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/autoconfigure/RateLimiterAutoConfiguration.java +++ b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/autoconfigure/RateLimiterAutoConfiguration.java @@ -16,29 +16,45 @@ package top.continew.starter.security.limiter.autoconfigure; +import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import top.continew.starter.security.limiter.aop.RateLimiterAspect; +import org.springframework.context.annotation.ComponentScan; +import top.continew.starter.core.constant.PropertiesConstants; +import top.continew.starter.security.limiter.core.DefaultRateLimiterNameGenerator; +import top.continew.starter.security.limiter.core.RateLimiterNameGenerator; /** - * 限流配置注入器 + * 限流器自动配置 * * @author KAI + * @author Charles7c * @since 2.2.0 */ - @AutoConfiguration @EnableConfigurationProperties(RateLimiterProperties.class) +@ComponentScan({"top.continew.starter.security.limiter.core"}) +@ConditionalOnProperty(prefix = PropertiesConstants.SECURITY_LIMITER, name = PropertiesConstants.ENABLED, matchIfMissing = true) public class RateLimiterAutoConfiguration { private static final Logger log = LoggerFactory.getLogger(RateLimiterAutoConfiguration.class); + /** + * 限流器名称生成器 + */ @Bean - public RateLimiterAspect rateLimiterAspect() { - log.info("[ContiNew Starter] - Auto Configuration 'RateLimiterAspect' completed initialization."); - return new RateLimiterAspect(); + @ConditionalOnMissingBean + public RateLimiterNameGenerator nameGenerator() { + return new DefaultRateLimiterNameGenerator(); + } + + @PostConstruct + public void postConstruct() { + log.debug("[ContiNew Starter] - Auto Configuration 'Security-RateLimiter' completed initialization."); } } diff --git a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/autoconfigure/RateLimiterProperties.java b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/autoconfigure/RateLimiterProperties.java index 6aacf487..db4c4c34 100644 --- a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/autoconfigure/RateLimiterProperties.java +++ b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/autoconfigure/RateLimiterProperties.java @@ -17,7 +17,7 @@ package top.continew.starter.security.limiter.autoconfigure; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.util.StringUtils; +import top.continew.starter.core.constant.PropertiesConstants; /** * 限流器配置属性 @@ -25,32 +25,19 @@ import org.springframework.util.StringUtils; * @author KAI * @since 2.2.0 */ -@ConfigurationProperties(prefix = "continew-starter.security.limiter") +@ConfigurationProperties(PropertiesConstants.SECURITY_LIMITER) public class RateLimiterProperties { - private boolean enabled = false; - private String limiterKey = "RateLimiter:"; + /** + * Key 前缀 + */ + private String keyPrefix = "RateLimiter"; - public boolean isEnabled() { - return enabled; + public String getKeyPrefix() { + return keyPrefix; } - public void setEnabled(boolean enabled) { - this.enabled = enabled; + public void setKeyPrefix(String keyPrefix) { + this.keyPrefix = keyPrefix; } - - public String getLimiterKey() { - return limiterKey; - } - - public void setLimiterKey(String limiterKey) { - //不为空且不以":"结尾,则添加":" - if (StringUtils.hasText(limiterKey)) { - if (!limiterKey.endsWith(":")) { - limiterKey = limiterKey + ":"; - } - } - this.limiterKey = limiterKey; - } - } diff --git a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/core/DefaultRateLimiterNameGenerator.java b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/core/DefaultRateLimiterNameGenerator.java new file mode 100644 index 00000000..c2a1a847 --- /dev/null +++ b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/core/DefaultRateLimiterNameGenerator.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.continew.starter.security.limiter.core; + +import cn.hutool.core.util.ClassUtil; +import top.continew.starter.core.constant.StringConstants; + +import java.lang.reflect.Method; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 默认限流器名称生成器 + * + * @author Charles7c + * @since 2.2.0 + */ +public class DefaultRateLimiterNameGenerator implements RateLimiterNameGenerator { + + protected final ConcurrentHashMap nameMap = new ConcurrentHashMap<>(); + + @Override + public String generate(Object target, Method method, Object... args) { + return nameMap.computeIfAbsent(method, key -> { + final StringBuilder nameSb = new StringBuilder(); + String className = method.getDeclaringClass().getName(); + nameSb.append(ClassUtil.getShortClassName(className)); + nameSb.append(StringConstants.DOT); + nameSb.append(method.getName()); + nameSb.append(StringConstants.ROUND_BRACKET_START); + for (Class clazz : method.getParameterTypes()) { + this.getDescriptor(nameSb, clazz); + } + nameSb.append(StringConstants.ROUND_BRACKET_END); + String str = nameSb.toString(); + nameMap.put(method, str); + return str; + }); + } + + /** + * 获取指定数据类型的描述 + * + * @param sb 名称字符串缓存 + * @param typeClass 数据类型 + */ + private void getDescriptor(final StringBuilder sb, final Class typeClass) { + Class clazz = typeClass; + while (true) { + if (clazz.isPrimitive()) { + sb.append(this.getPrimitiveChar(clazz)); + return; + } else if (clazz.isArray()) { + sb.append(StringConstants.BRACKET_START); + clazz = clazz.getComponentType(); + } else { + sb.append('L'); + String name = clazz.getName(); + name = ClassUtil.getShortClassName(name); + sb.append(name); + sb.append(StringConstants.COLON); + return; + } + } + } + + /** + * 根据基本数据获取类型字符 + * + * @param clazz 数据类型 + * @return 类型字符 + */ + private char getPrimitiveChar(Class clazz) { + char c; + if (clazz == Integer.TYPE) { + c = 'I'; + } else if (clazz == Void.TYPE) { + c = 'V'; + } else if (clazz == Boolean.TYPE) { + c = 'Z'; + } else if (clazz == Byte.TYPE) { + c = 'B'; + } else if (clazz == Character.TYPE) { + c = 'C'; + } else if (clazz == Short.TYPE) { + c = 'S'; + } else if (clazz == Double.TYPE) { + c = 'D'; + } else if (clazz == Float.TYPE) { + c = 'F'; + } else { + c = 'J'; + } + return c; + } +} diff --git a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/core/RateLimiterAspect.java b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/core/RateLimiterAspect.java new file mode 100644 index 00000000..007b7086 --- /dev/null +++ b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/core/RateLimiterAspect.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.continew.starter.security.limiter.core; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.servlet.JakartaServletUtil; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.redisson.api.*; +import org.springframework.stereotype.Component; +import top.continew.starter.cache.redisson.util.RedisUtils; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.util.expression.ExpressionUtils; +import top.continew.starter.security.limiter.annotation.RateLimiter; +import top.continew.starter.security.limiter.annotation.RateLimiters; +import top.continew.starter.security.limiter.autoconfigure.RateLimiterProperties; +import top.continew.starter.security.limiter.enums.LimitType; +import top.continew.starter.security.limiter.exception.RateLimiterException; +import top.continew.starter.web.util.ServletUtils; + +import java.lang.reflect.Method; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 限流器切面 + * + * @author KAI + * @author Charles7c + * @since 2.2.0 + */ +@Aspect +@Component +public class RateLimiterAspect { + + private static final ConcurrentHashMap RATE_LIMITER_CACHE = new ConcurrentHashMap<>(); + private final RateLimiterProperties properties; + private final RateLimiterNameGenerator nameGenerator; + private final RedissonClient redissonClient; + + public RateLimiterAspect(RateLimiterProperties properties, + RateLimiterNameGenerator nameGenerator, + RedissonClient redissonClient) { + this.properties = properties; + this.nameGenerator = nameGenerator; + this.redissonClient = redissonClient; + } + + /** + * 单个限流注解切点 + */ + @Pointcut("@annotation(top.continew.starter.security.limiter.annotation.RateLimiter)") + public void rateLimiterPointCut() { + } + + /** + * 多个限流注解切点 + */ + @Pointcut("@annotation(top.continew.starter.security.limiter.annotation.RateLimiters)") + public void rateLimitersPointCut() { + } + + /** + * 单限流场景 + * + * @param joinPoint 切点 + * @param rateLimiter 限流注解 + * @return 目标方法的执行结果 + * @throws Throwable / + */ + @Around("@annotation(rateLimiter)") + public Object aroundRateLimiter(ProceedingJoinPoint joinPoint, RateLimiter rateLimiter) throws Throwable { + if (isRateLimited(joinPoint, rateLimiter)) { + throw new RateLimiterException(rateLimiter.message()); + } + return joinPoint.proceed(); + } + + /** + * 多限流场景 + * + * @param joinPoint 切点 + * @param rateLimiters 限流组注解 + * @return 目标方法的执行结果 + * @throws Throwable / + */ + @Around("@annotation(rateLimiters)") + public Object aroundRateLimiters(ProceedingJoinPoint joinPoint, RateLimiters rateLimiters) throws Throwable { + for (RateLimiter rateLimiter : rateLimiters.value()) { + if (isRateLimited(joinPoint, rateLimiter)) { + throw new RateLimiterException(rateLimiter.message()); + } + } + return joinPoint.proceed(); + } + + /** + * 是否需要限流 + * + * @param joinPoint 切点 + * @param rateLimiter 限流注解 + * @return true: 需要限流;false:不需要限流 + */ + private boolean isRateLimited(ProceedingJoinPoint joinPoint, RateLimiter rateLimiter) { + try { + String cacheKey = this.getCacheKey(joinPoint, rateLimiter); + RRateLimiter rRateLimiter = RATE_LIMITER_CACHE.computeIfAbsent(cacheKey, key -> redissonClient + .getRateLimiter(cacheKey)); + // 限流器配置 + RateType rateType = rateLimiter.type() == LimitType.CLUSTER ? RateType.PER_CLIENT : RateType.OVERALL; + int rate = rateLimiter.rate(); + int rateInterval = rateLimiter.interval(); + RateIntervalUnit rateIntervalUnit = rateLimiter.unit(); + // 判断是否需要更新限流器 + if (this.isConfigurationUpdateNeeded(rRateLimiter, rateType, rate, rateInterval, rateIntervalUnit)) { + // 更新限流器 + rRateLimiter.setRate(rateType, rate, rateInterval, rateIntervalUnit); + } + // 尝试获取令牌 + return !rRateLimiter.tryAcquire(); + } catch (Exception e) { + throw new RateLimiterException("服务器限流异常,请稍候再试", e); + } + } + + /** + * 获取限流缓存 Key + * + * @param joinPoint 切点 + * @param rateLimiter 限流注解 + * @return 限流缓存 Key + */ + private String getCacheKey(JoinPoint joinPoint, RateLimiter rateLimiter) { + Object target = joinPoint.getTarget(); + MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature(); + Method method = methodSignature.getMethod(); + Object[] args = joinPoint.getArgs(); + // 获取限流名称 + String name = rateLimiter.name(); + if (CharSequenceUtil.isBlank(name)) { + name = nameGenerator.generate(target, method, args); + } + // 解析限流 Key + String key = rateLimiter.key(); + if (CharSequenceUtil.isNotBlank(key)) { + Object eval = ExpressionUtils.eval(key, target, method, args); + if (ObjectUtil.isNull(eval)) { + throw new RateLimiterException("限流 Key 解析错误"); + } + key = Convert.toStr(eval); + } + // 获取后缀 + String suffix = switch (rateLimiter.type()) { + case IP -> JakartaServletUtil.getClientIP(ServletUtils.getRequest()); + case CLUSTER -> redissonClient.getId(); + default -> StringConstants.EMPTY; + }; + return RedisUtils.formatKey(properties.getKeyPrefix(), name, key, suffix); + } + + /** + * 判断是否需要更新限流器配置 + * + * @param rRateLimiter 限流器 + * @param rateType 限流类型(OVERALL:全局限流;PER_CLIENT:单机限流) + * @param rate 速率(指定时间间隔产生的令牌数) + * @param rateInterval 速率间隔 + * @param rateIntervalUnit 时间单位 + * @return 是否需要更新配置 + */ + private boolean isConfigurationUpdateNeeded(RRateLimiter rRateLimiter, + RateType rateType, + long rate, + long rateInterval, + RateIntervalUnit rateIntervalUnit) { + RateLimiterConfig config = rRateLimiter.getConfig(); + return !Objects.equals(config.getRateType(), rateType) || !Objects.equals(config.getRate(), rate) || !Objects + .equals(config.getRateInterval(), rateIntervalUnit.toMillis(rateInterval)); + } +} diff --git a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/core/RateLimiterNameGenerator.java b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/core/RateLimiterNameGenerator.java new file mode 100644 index 00000000..78ddd6aa --- /dev/null +++ b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/core/RateLimiterNameGenerator.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.continew.starter.security.limiter.core; + +import java.lang.reflect.Method; + +/** + * 限流器名称生成器 + * + * @author Charles7c + * @since 2.2.0 + */ +@FunctionalInterface +public interface RateLimiterNameGenerator { + + /** + * Generate a rate limiter name for the given method and its parameters. + * + * @param target the target instance + * @param method the method being called + * @param args the method parameters (with any var-args expanded) + * @return a generated rate limiter name + */ + String generate(Object target, Method method, Object... args); +} diff --git a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/enums/LimitType.java b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/enums/LimitType.java index 65922c7c..e8b25688 100644 --- a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/enums/LimitType.java +++ b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/enums/LimitType.java @@ -28,10 +28,12 @@ public enum LimitType { * 全局限流 */ DEFAULT, + /** - * 根据IP限流 + * 根据 IP 限流 */ IP, + /** * 根据实例限流(支持集群多实例) */ diff --git a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/exception/RateLimiterException.java b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/exception/RateLimiterException.java index 669f2f1c..accd1d30 100644 --- a/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/exception/RateLimiterException.java +++ b/continew-starter-security/continew-starter-security-limiter/src/main/java/top/continew/starter/security/limiter/exception/RateLimiterException.java @@ -25,7 +25,12 @@ import top.continew.starter.core.exception.BaseException; * @since 2.2.0 */ public class RateLimiterException extends BaseException { + public RateLimiterException(String message) { super(message); } + + public RateLimiterException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/continew-starter-security/pom.xml b/continew-starter-security/pom.xml index 84df7437..1a0f4f54 100644 --- a/continew-starter-security/pom.xml +++ b/continew-starter-security/pom.xml @@ -26,12 +26,5 @@ top.continew continew-starter-core - - - - top.continew - continew-starter-web - - \ No newline at end of file