feat(security/limiter): 新增限流器

This commit is contained in:
KAI
2024-06-25 01:01:43 +00:00
committed by Charles7c
parent 3e9a15295a
commit a89765f49e
14 changed files with 622 additions and 0 deletions

View File

@@ -206,6 +206,25 @@ public class RedisUtils {
return rateLimiter.tryAcquire(1); return rateLimiter.tryAcquire(1);
} }
/**
* 限流
*
* @param key 限流key
* @param rateType 限流类型
* @param rate 速率
* @param rateInterval 速率间隔
* @return -1 表示失败
*/
public static long rateLimiter(String key, RateType rateType, int rate, int rateInterval, RateIntervalUnit unit) {
RRateLimiter rateLimiter = CLIENT.getRateLimiter(key);
rateLimiter.trySetRate(rateType, rate, rateInterval, unit);
if (rateLimiter.tryAcquire()) {
return rateLimiter.availablePermits();
} else {
return -1L;
}
}
/** /**
* 格式化键,将各子键用 : 拼接起来 * 格式化键,将各子键用 : 拼接起来
* *

View File

@@ -473,6 +473,13 @@
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<!-- 安全模块 - 限流 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-security-limiter</artifactId>
<version>${revision}</version>
</dependency>
<!-- API 文档模块 --> <!-- API 文档模块 -->
<dependency> <dependency>
<groupId>top.continew</groupId> <groupId>top.continew</groupId>

View File

@@ -0,0 +1,42 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter-security</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-security-limiter</artifactId>
<description>ContiNew Starter 安全模块 - 限流模块</description>
<dependencies>
<!-- Redisson不仅仅是一个 Redis Java 客户端Redisson 充分的利用了 Redis 键值数据库提供的一系列优势,为使用者提供了一系列具有分布式特性的常用工具:分布式锁、限流器等) -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
<!--aop-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--Redisson缓存 模块-->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-cache-redisson</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,71 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* 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
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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.annotation;
import org.redisson.api.RateIntervalUnit;
import top.continew.starter.security.limiter.enums.LimitType;
import java.lang.annotation.*;
/**
* 限流注解
* @author KAI
* @since 2.2.0
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
/**
* LimitType 限流模式
* DEFAULT 全局限流
* IP IP限流
* CLUSTER 实例限流
*/
LimitType limitType() default LimitType.DEFAULT;
/**
* 缓存实例名称
*/
String name() default "";
/**
* 限流key 支持 Spring EL 表达式
*/
String key() default "";
/**
* 单位时间产生的令牌数
*/
int rate() default Integer.MAX_VALUE;
/**
* 限流时间
*/
int rateInterval() default 0;
/**
* 时间单位,默认毫秒
*/
RateIntervalUnit timeUnit() default RateIntervalUnit.MILLISECONDS;
/**
* 拒绝请求时的提示信息
*/
String message() default "您操作过于频繁,请稍后再试!";
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* 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
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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.annotation;
import java.lang.annotation.*;
/**
* 限流组
* @author KAI
* @since 2.2.0
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiters {
/**
* 用于管理多个 RateLimiter
*/
RateLimiter[] value();
}

View File

@@ -0,0 +1,265 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* 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
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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<String, RRateLimiter> 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();
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* 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
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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.autoconfigure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import top.continew.starter.security.limiter.aop.RateLimiterAspect;
/**
* 限流配置注入器
* @author KAI
* @since 2.2.0
*/
@AutoConfiguration
@EnableConfigurationProperties(RateLimiterProperties.class)
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();
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* 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
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StringUtils;
/**
* 限流器配置属性
* @author KAI
* @since 2.2.0
*/
@ConfigurationProperties(prefix = "continew-starter.security.limiter")
public class RateLimiterProperties {
private boolean enabled = false;
private String limiterKey = "RateLimiter:";
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getLimiterKey() {
return limiterKey;
}
public void setLimiterKey(String limiterKey) {
//不为空且不以":"结尾,则添加":"
if (StringUtils.hasText(limiterKey)) {
if (!limiterKey.endsWith(":")) {
limiterKey = limiterKey + ":";
}
}
this.limiterKey = limiterKey;
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* 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
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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.enums;
/**
* 限流类型
* @author KAI
* @since 2.2.0
*/
public enum LimitType {
/**
* 全局限流
*/
DEFAULT,
/**
* 根据IP限流
*/
IP,
/**
* 根据实例限流(支持集群多实例)
*/
CLUSTER
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* 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
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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.exception;
import top.continew.starter.core.exception.BaseException;
/**
* 限流异常
* @author KAI
* @since 2.2.0
*/
public class RateLimiterException extends BaseException {
public RateLimiterException(String message) {
super(message);
}
}

View File

@@ -0,0 +1 @@
top.continew.starter.security.limiter.autoconfigure.RateLimiterAutoConfiguration

View File

@@ -0,0 +1,7 @@
{
"top.continew.starter.security.limiter.annotation.RateLimiter@key":{
"method":{
"parameters": true
}
}
}

View File

@@ -17,6 +17,7 @@
<module>continew-starter-security-password</module> <module>continew-starter-security-password</module>
<module>continew-starter-security-mask</module> <module>continew-starter-security-mask</module>
<module>continew-starter-security-crypto</module> <module>continew-starter-security-crypto</module>
<module>continew-starter-security-limiter</module>
</modules> </modules>
<dependencies> <dependencies>
@@ -25,5 +26,12 @@
<groupId>top.continew</groupId> <groupId>top.continew</groupId>
<artifactId>continew-starter-core</artifactId> <artifactId>continew-starter-core</artifactId>
</dependency> </dependency>
<!-- Web 模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-web</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -44,6 +44,8 @@ import top.continew.starter.web.autoconfigure.i18n.I18nProperties;
import top.continew.starter.web.model.R; import top.continew.starter.web.model.R;
import top.continew.starter.web.util.MessageSourceUtils; import top.continew.starter.web.util.MessageSourceUtils;
import java.awt.image.RasterFormatException;
/** /**
* 全局异常处理器 * 全局异常处理器
* *
@@ -195,4 +197,5 @@ public class GlobalExceptionHandler {
log.error("请求地址 [{}],发生未知异常。", request.getRequestURI(), e); log.error("请求地址 [{}],发生未知异常。", request.getRequestURI(), e);
return R.fail(e.getMessage()); return R.fail(e.getMessage());
} }
} }