This commit is contained in:
kk
2026-07-07 18:01:21 +08:00
commit b259f94d4b
1088 changed files with 121778 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<artifactId>BladeX-Tool</artifactId>
<groupId>org.springblade</groupId>
<version>${revision}</version>
</parent>
<artifactId>blade-starter-ratelimit</artifactId>
<name>${project.artifactId}</name>
<version>${project.parent.version}</version>
<packaging>jar</packaging>
<properties>
<module.name>org.springblade.blade.starter.ratelimit</module.name>
</properties>
<dependencies>
<!--Redis 限流原语-->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-redis</artifactId>
</dependency>
<!--缓存工具-->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-cache</artifactId>
</dependency>
<!--JdbcTemplate 默认规则装载-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<!-- Auto -->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-core-auto</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,51 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.ratelimit;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;
import static org.springblade.core.ratelimit.RateLimitConstant.RULE_SELECT_STATEMENT;
/**
* 限流规则处理器默认实现,经本地数据源直取,适用于单体及持有规则表的服务。
*
* @author Chill
*/
@RequiredArgsConstructor
public class BladeRateLimitHandler implements IRateLimitHandler {
private final JdbcTemplate jdbcTemplate;
@Override
public List<RateLimitRule> listEnabledRules() {
return jdbcTemplate.query(RULE_SELECT_STATEMENT, new BeanPropertyRowMapper<>(RateLimitRule.class));
}
}
@@ -0,0 +1,119 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.ratelimit;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.launch.props.BladeProperties;
import org.springblade.core.launch.server.ServerInfo;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.constant.BladeConstant;
import org.springblade.core.tool.support.IdGenerator;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.UrlUtil;
import org.springblade.core.tool.utils.WebUtil;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static org.springblade.core.ratelimit.RateLimitConstant.RATE_LIMIT_LOG_INSERT;
/**
* 限流触发日志处理器默认实现。
* <p>
* 在主线程采集触发上下文 (请求信息与登录态), 经有界单线程池异步落库, 异常仅记录不影响主流程。
*
* @author Chill
*/
@Slf4j
@AllArgsConstructor
public class BladeRateLimitLogHandler implements IRateLimitLogHandler {
/**
* 日志落库专用线程池。
* <p>
* 限流被触发往往伴随高频请求, 直接走 {@code commonPool} 无界提交会瞬时打满线程与数据库连接。
* 此处以单线程串行落库 + 有界队列收口, 队列溢出 (洪峰拒绝) 时丢弃多余日志, 保住主流程与库连接。
*/
private static final ExecutorService LOG_EXECUTOR = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1024),
runnable -> {
Thread thread = new Thread(runnable, "rate-limit-log");
thread.setDaemon(true);
return thread;
},
new ThreadPoolExecutor.DiscardPolicy());
private final JdbcTemplate jdbcTemplate;
private final BladeProperties bladeProperties;
private final ServerInfo serverInfo;
@Override
public void saveLog(RateLimitRule rule, String limitKey, HttpServletRequest request) {
// 在主线程采集参数 (请求与登录态上下文此刻有效), 仅落库动作交由有界线程池异步执行
Object[] params = collectLogParams(rule, limitKey, request);
LOG_EXECUTOR.execute(() -> {
try {
jdbcTemplate.update(RATE_LIMIT_LOG_INSERT, params);
} catch (Exception logException) {
log.error("限流日志保存失败: {}", logException.getMessage());
}
});
}
/**
* 采集日志参数
*
* @param rule 触发的限流规则
* @param limitKey 实际限流计数 key
* @param request 当前请求对象
* @return SQL 参数数组
*/
private Object[] collectLogParams(RateLimitRule rule, String limitKey, HttpServletRequest request) {
String tenantId = Func.toStrWithEmpty(AuthUtil.getTenantId(), BladeConstant.ADMIN_TENANT_ID);
String createBy = Func.toStrWithEmpty(AuthUtil.getUserAccount(), StringPool.EMPTY);
String requestUri = UrlUtil.getPath(request.getRequestURI());
String httpMethod = request.getMethod();
String remoteIp = WebUtil.getIP(request);
String userAgent = Func.toStr(request.getHeader(WebUtil.USER_AGENT_HEADER));
String params = WebUtil.getRequestContent(request);
return new Object[]{
IdGenerator.getId(), tenantId,
bladeProperties.getName(), serverInfo.getIpWithPort(), serverInfo.getHostName(), bladeProperties.getEnv(),
rule.getId(), rule.getLimitName(), rule.getLimitDimension(), rule.getLimitCount(), rule.getLimitPeriod(), rule.getTimeUnit(), limitKey,
requestUri, httpMethod, remoteIp, userAgent, params,
createBy, new Date()
};
}
}
@@ -0,0 +1,46 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.ratelimit;
import java.util.List;
/**
* 限流规则处理器接口。
* <p>
* 抽象规则的取数来源:单体经本地数据源直取,微服务经 Feign 远程取数。
*
* @author Chill
*/
public interface IRateLimitHandler {
/**
* 获取全部启用的限流规则,按优先级升序、跨租户全局。
*
* @return 启用的限流规则集合,无规则时返回空集
*/
List<RateLimitRule> listEnabledRules();
}
@@ -0,0 +1,48 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.ratelimit;
import jakarta.servlet.http.HttpServletRequest;
/**
* 限流触发日志处理器接口。
* <p>
* 请求被限流时记录触发上下文。下游工程可自定义实现以支持不同的日志存储策略 (如消息队列、远程服务等)。
*
* @author Chill
*/
public interface IRateLimitLogHandler {
/**
* 保存限流触发日志。在主请求线程中调用, 实现类可在内部自行决定同步或异步策略。
*
* @param rule 触发的限流规则
* @param limitKey 实际限流计数 key
* @param request 当前请求对象
*/
void saveLog(RateLimitRule rule, String limitKey, HttpServletRequest request);
}
@@ -0,0 +1,65 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.ratelimit;
/**
* 限流常量。
*
* @author Chill
*/
public interface RateLimitConstant {
/**
* 计数 key 前缀, 底层 RateLimiterClient 会再追加 limiter:{应用名}: 前缀
*/
String KEY_PREFIX = "rateLimit:";
/**
* 匹配全部请求方法的标识
*/
String METHOD_ALL = "ALL";
/**
* 规则刷新广播频道, 各节点据此热更新本地快照 (须为精确频道, 不含通配符)
*/
String RULE_REFRESH_CHANNEL = "blade:rate-limit:rule:refresh";
/**
* 加载全部启用规则的 SQL (裸 SQL 天然跨租户全局加载, 按优先级升序)
*/
String RULE_SELECT_STATEMENT =
"SELECT id, limit_name, limit_pattern, request_method, limit_dimension, limit_count, limit_period, time_unit, priority, remark "
+ "FROM blade_rate_limit WHERE status = 1 AND is_deleted = 0 ORDER BY priority ASC";
/**
* 插入限流触发日志的 SQL
*/
String RATE_LIMIT_LOG_INSERT =
"INSERT INTO blade_rate_limit_log "
+ "(id, tenant_id, service_id, server_ip, server_host, env, limit_id, limit_name, limit_dimension, limit_count, limit_period, time_unit, limit_key, request_uri, method, remote_ip, user_agent, params, create_by, create_time) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
}
@@ -0,0 +1,160 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.ratelimit;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.ratelimit.enums.RateLimitDimension;
import org.springblade.core.redis.ratelimiter.RateLimiterClient;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.SpringUtil;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.tool.utils.WebUtil;
import org.springframework.http.HttpStatus;
import org.springframework.lang.NonNull;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 限流拦截器。
* <p>
* 规则按优先级遍历,首条命中 (请求方法 + Ant 路径) 者即按其维度构造计数 key 交 {@link RateLimiterClient} 判定,超限返回 429。
* 规则取自 {@link RateLimitRuleManager} 的进程内快照,拦截器不感知其来源 (本地直取 / 远程 Feign) 与同步机制。
*
* @author Chill
*/
@Slf4j
@RequiredArgsConstructor
public class RateLimitInterceptor implements HandlerInterceptor {
/**
* 滑动窗口限流客户端, 复用 blade-starter-redis 实现
*/
private final RateLimiterClient rateLimiterClient;
/**
* 触发限流时的提示文案
*/
private final String rejectMessage;
/**
* 规则匹配前缀, 拼回网关剥离的服务前缀后再与规则比对 (单体为空)
*/
private final String pathPrefix;
/**
* Ant 风格路径匹配器, 无状态线程安全, 内部对编译后的 pattern 有缓存
*/
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private RateLimitRuleManager ruleManager;
private IRateLimitLogHandler rateLimitLogHandler;
private RateLimitRuleManager getRuleManager() {
if (ruleManager == null) {
ruleManager = SpringUtil.getBean(RateLimitRuleManager.class);
}
return ruleManager;
}
private IRateLimitLogHandler getRateLimitLogHandler() {
if (rateLimitLogHandler == null) {
rateLimitLogHandler = SpringUtil.getBean(IRateLimitLogHandler.class);
}
return rateLimitLogHandler;
}
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
List<RateLimitRule> rules = getRuleManager().listEnabledRules();
if (rules.isEmpty()) {
return true;
}
String uri = pathPrefix.concat(request.getRequestURI());
String method = request.getMethod();
for (RateLimitRule rule : rules) {
if (!methodMatches(rule.getRequestMethod(), method) || !pathMatches(rule.getLimitPattern(), uri)) {
continue;
}
String limitKey = RateLimitDimension.of(rule.getLimitDimension()).obtainKey(rule.getId(), request);
// 维度值缺失 (如匿名请求命中用户/租户维度规则), 跳过该规则继续匹配后续规则
if (limitKey == null) {
continue;
}
boolean allowed = rateLimiterClient.isAllowed(limitKey, rule.getLimitCount(), rule.getLimitPeriod(), TimeUnit.valueOf(rule.getTimeUnit()));
if (allowed) {
return true;
}
log.warn("请求触发限流, uri={}, ip={}, rule={}, dimension={}, rate={}/{} {}", uri, WebUtil.getIP(request), rule.getLimitName(),
rule.getLimitDimension(), rule.getLimitCount(), rule.getLimitPeriod(), rule.getTimeUnit());
getRateLimitLogHandler().saveLog(rule, limitKey, request);
reject(response);
return false;
}
return true;
}
/**
* 请求方法是否匹配规则。规则方法为空或 ALL 时匹配全部方法。
*/
private boolean methodMatches(String ruleMethod, String requestMethod) {
return StringUtil.isBlank(ruleMethod) || RateLimitConstant.METHOD_ALL.equalsIgnoreCase(ruleMethod) || ruleMethod.equalsIgnoreCase(requestMethod);
}
/**
* 请求 URL 是否匹配规则的匹配模式。
* <p>
* 匹配模式支持逗号分隔的多个 Ant 风格 pattern (一条规则覆盖多个 URL),命中任意一个即视为匹配;
* 这些 URL 共享该规则的同一份配额 (计数 key 仅以 ruleId 与维度区分,与具体命中的 URL 无关)。
*/
private boolean pathMatches(String limitPattern, String uri) {
if (StringUtil.isBlank(limitPattern)) {
return false;
}
for (String pattern : limitPattern.split(StringPool.COMMA)) {
String trimmedPattern = pattern.trim();
if (StringUtil.isNotBlank(trimmedPattern) && pathMatcher.match(trimmedPattern, uri)) {
return true;
}
}
return false;
}
/**
* 输出 429 限流响应。
*/
private void reject(HttpServletResponse response) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
WebUtil.renderJson(response, R.fail(HttpStatus.TOO_MANY_REQUESTS.value(), rejectMessage));
}
}
@@ -0,0 +1,96 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.ratelimit;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 限流规则模型。
* <p>
* 系统级传输/缓存载体,供默认实现(JdbcTemplate)与微服务实现(Feign)共用,与业务层带租户基类的实体相互独立。
*
* @author Chill
*/
@Data
public class RateLimitRule implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private Long id;
/**
* 规则名称
*/
private String limitName;
/**
* URL匹配模式, 支持 Ant 风格, 多个以逗号分隔 (命中任意一个即匹配, 共享同一份配额)
*/
private String limitPattern;
/**
* 请求方法, GET/POST/PUT/DELETE/ALL
*/
private String requestMethod;
/**
* 限流维度, GLOBAL/TENANT/IP/USER
*/
private String limitDimension;
/**
* 时间窗口内允许的最大请求数
*/
private Integer limitCount;
/**
* 时间窗口大小
*/
private Integer limitPeriod;
/**
* 时间单位, SECONDS/MINUTES/HOURS
*/
private String timeUnit;
/**
* 优先级, 升序匹配
*/
private Integer priority;
/**
* 备注
*/
private String remark;
}
@@ -0,0 +1,153 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.ratelimit;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.redis.pubsub.RPubSubEvent;
import org.springblade.core.redis.pubsub.RPubSubListener;
import org.springblade.core.redis.pubsub.RPubSubPublisher;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.springblade.core.ratelimit.RateLimitConstant.RULE_REFRESH_CHANNEL;
/**
* 限流规则的进程内快照管理器。
* <p>
* 读路径无锁读取 volatile 快照,将每请求一次的远程取数收敛为一次内存读取;规则变更在事务提交后于本节点重建,
* 并经 Redis 广播驱动其余节点热更新,偶发漏收的广播由刷新间隔兜底收敛。取数方式由 {@link IRateLimitHandler}
* 决定,使单体与微服务共用同一套快照与同步逻辑。
*
* @author Chill
*/
@Slf4j
@RequiredArgsConstructor
public class RateLimitRuleManager {
private final IRateLimitHandler ruleHandler;
private final RPubSubPublisher publisher;
private final long refreshIntervalMillis;
/**
* 实例标识,用于识别并忽略本节点自身发出的广播
*/
private final String instanceId = UUID.randomUUID().toString();
/**
* 异步重建去重标记,避免快照过期瞬间的并发取数
*/
private final AtomicBoolean rebuilding = new AtomicBoolean();
/**
* 当前规则快照,null 表示尚未加载,空集表示已加载且无启用规则
*/
private volatile List<RateLimitRule> snapshot;
/**
* 快照最近一次加载的时间戳,用于刷新间隔的过期判定
*/
private volatile long loadedAt;
/**
* 获取当前启用的限流规则,供拦截器逐请求调用。快照未就绪时返回空集 (本次放行) 并触发异步加载,
* 不在请求线程上阻塞远程取数。
*
* @return 规则快照,不可变且非空
*/
public List<RateLimitRule> listEnabledRules() {
List<RateLimitRule> current = snapshot;
if (current == null) {
asyncRebuild();
return List.of();
}
if (System.currentTimeMillis() - loadedAt > refreshIntervalMillis) {
asyncRebuild();
}
return current;
}
/**
* 规则增删改后由业务侧调用。处于事务中时延迟至提交后执行,避免其余节点读到未提交数据;
* 提交后本节点立即重建并广播全集群。
*/
public void refresh() {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
rebuildAndBroadcast();
}
});
} else {
rebuildAndBroadcast();
}
}
/**
* 接收集群广播并重建本节点快照,忽略自身消息。
*/
@RPubSubListener(RULE_REFRESH_CHANNEL)
public void onRemoteRefresh(RPubSubEvent<String> event) {
if (!instanceId.equals(event.getMsg())) {
rebuild();
}
}
private void rebuildAndBroadcast() {
try {
rebuild();
publisher.publish(RULE_REFRESH_CHANNEL, instanceId);
} catch (Exception broadcastException) {
log.warn("限流规则广播失败,将由刷新间隔兜底收敛: {}", broadcastException.getMessage());
}
}
private void rebuild() {
List<RateLimitRule> rules = ruleHandler.listEnabledRules();
snapshot = (rules == null) ? List.of() : List.copyOf(rules);
loadedAt = System.currentTimeMillis();
}
private void asyncRebuild() {
if (rebuilding.compareAndSet(false, true)) {
CompletableFuture.runAsync(this::rebuild).whenComplete((ignored, error) -> {
rebuilding.set(false);
if (error != null) {
log.warn("限流规则加载失败,下次请求重试: {}", error.getMessage());
}
});
}
}
}
@@ -0,0 +1,74 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.ratelimit.config;
import org.springblade.core.launch.props.BladeProperties;
import org.springblade.core.launch.server.ServerInfo;
import org.springblade.core.ratelimit.BladeRateLimitHandler;
import org.springblade.core.ratelimit.BladeRateLimitLogHandler;
import org.springblade.core.ratelimit.IRateLimitHandler;
import org.springblade.core.ratelimit.IRateLimitLogHandler;
import org.springblade.core.ratelimit.RateLimitRuleManager;
import org.springblade.core.ratelimit.props.RateLimitProperties;
import org.springblade.core.redis.pubsub.RPubSubPublisher;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* 限流默认配置。
* <p>
* 装配规则取数实现 {@link IRateLimitHandler} (默认本地 JdbcTemplate, 微服务侧由 Feign 实现覆盖) 与统一的规则管理器
* {@link RateLimitRuleManager}, 以及触发日志处理器 {@link IRateLimitLogHandler}, 均以 {@link ConditionalOnMissingBean}
* 兜底允许下游覆盖。管理器恒定可用 (供规则变更广播), 是否真正拦截由 {@code blade.ratelimit.enabled} 控制。
*
* @author Chill
*/
@AutoConfiguration
@EnableConfigurationProperties(RateLimitProperties.class)
public class RateLimitAutoConfiguration {
@Bean
@ConditionalOnMissingBean(IRateLimitHandler.class)
public IRateLimitHandler rateLimitHandler(JdbcTemplate jdbcTemplate) {
return new BladeRateLimitHandler(jdbcTemplate);
}
@Bean
@ConditionalOnMissingBean(IRateLimitLogHandler.class)
public IRateLimitLogHandler rateLimitLogHandler(JdbcTemplate jdbcTemplate, BladeProperties bladeProperties, ServerInfo serverInfo) {
return new BladeRateLimitLogHandler(jdbcTemplate, bladeProperties, serverInfo);
}
@Bean
@ConditionalOnMissingBean
public RateLimitRuleManager rateLimitRuleManager(IRateLimitHandler ruleHandler, RPubSubPublisher publisher, RateLimitProperties properties) {
return new RateLimitRuleManager(ruleHandler, publisher, properties.getRefreshInterval().toMillis());
}
}
@@ -0,0 +1,86 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.ratelimit.config;
import lombok.AllArgsConstructor;
import org.springblade.core.ratelimit.RateLimitInterceptor;
import org.springblade.core.ratelimit.props.RateLimitProperties;
import org.springblade.core.redis.ratelimiter.RateLimiterClient;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
/**
* 限流拦截器注册配置。
* <p>
* 将 {@link RateLimitInterceptor} 注册到 MVC 拦截器链,对全部请求生效。放行路径由内置默认列表与
* {@link RateLimitProperties#getExcludePatterns()} 合并而成。
*
* @author Chill
*/
@AutoConfiguration
@AllArgsConstructor
@EnableConfigurationProperties(RateLimitProperties.class)
@ConditionalOnProperty(value = "blade.ratelimit.enabled", havingValue = "true")
public class RateLimitWebConfiguration implements WebMvcConfigurer {
/**
* 内置默认放行路径 (文档、监控、静态资源、内部 Feign 等)
*/
private static final List<String> DEFAULT_EXCLUDE_PATTERNS = List.of(
"/doc.html",
"/swagger-ui.html",
"/swagger-resources/**",
"/v3/api-docs/**",
"/webjars/**",
"/static/**",
"/druid/**",
"/error",
"/actuator/**",
"/feign/**"
);
private final RateLimiterClient rateLimiterClient;
private final RateLimitProperties rateLimitProperties;
@Override
public void addInterceptors(@NonNull InterceptorRegistry registry) {
List<String> excludePatterns = new ArrayList<>(DEFAULT_EXCLUDE_PATTERNS);
excludePatterns.addAll(rateLimitProperties.getExcludePatterns());
RateLimitInterceptor interceptor = new RateLimitInterceptor(rateLimiterClient, rateLimitProperties.getRejectMessage(), rateLimitProperties.getPathPrefix());
registry.addInterceptor(interceptor)
.addPathPatterns(rateLimitProperties.getIncludePatterns())
.excludePathPatterns(excludePatterns);
}
}
@@ -0,0 +1,100 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.ratelimit.enums;
import jakarta.servlet.http.HttpServletRequest;
import org.springblade.core.ratelimit.RateLimitConstant;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.tool.utils.WebUtil;
/**
* 限流维度, 决定限流计数的隔离粒度与 Redis 计数 key 的构造方式。
*
* @author Chill
*/
public enum RateLimitDimension {
/**
* 全局: 同一规则所有请求共享一份配额
*/
GLOBAL,
/**
* 租户: 同一规则按租户分别计数, 适用于 SaaS 多租户配额
*/
TENANT,
/**
* IP: 按来源 IP 分别计数, 适用于匿名防刷
*/
IP,
/**
* 用户: 按登录用户分别计数, 适用于登录用户配额
*/
USER;
/**
* 解析限流维度, 非法值回退为 GLOBAL, 避免脏数据导致规则失效
*
* @param dimension 维度编码
* @return 限流维度
*/
public static RateLimitDimension of(String dimension) {
for (RateLimitDimension value : values()) {
if (value.name().equalsIgnoreCase(dimension)) {
return value;
}
}
return GLOBAL;
}
/**
* 构造限流计数 key。当维度值无法获取时 (如匿名请求命中 USER/TENANT 维度规则) 返回 null,
* 调用方据此跳过该规则、继续匹配后续规则。
*
* @param ruleId 规则主键, 作为 key 后缀保证短而唯一
* @param request 当前请求
* @return 计数 key, 维度值缺失时为 null
*/
public String obtainKey(Long ruleId, HttpServletRequest request) {
String base = RateLimitConstant.KEY_PREFIX + ruleId;
return switch (this) {
case GLOBAL -> base;
case IP -> base + ":ip:" + WebUtil.getIP(request);
case TENANT -> {
String tenantId = AuthUtil.getTenantId();
yield StringUtil.isBlank(tenantId) ? null : base + ":t:" + tenantId;
}
case USER -> {
Long userId = AuthUtil.getUserId();
yield (userId == null || userId <= 0L) ? null : base + ":u:" + userId;
}
};
}
}
@@ -0,0 +1,77 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.ratelimit.env;
import org.springblade.core.auto.annotation.AutoEnvPostProcessor;
import org.springblade.core.launch.utils.PropsUtil;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.util.ClassUtils;
/**
* 限流配置初始化。
* <p>
* 引入本 starter 即开启底层 Redis 限流原语;微服务侧 (存在 blade-core-cloud) 再将 {@code path-prefix}
* 落值为服务名,补回网关剥离的前缀,使规则匹配在单体与微服务下对齐。
*
* @author Chill
*/
@AutoEnvPostProcessor
public class RateLimitEnvPostProcessor implements EnvironmentPostProcessor, Ordered {
private static final String REDIS_RATE_LIMITER_ENABLED_KEY = "blade.redis.rate-limiter.enabled";
private static final String PATH_PREFIX_KEY = "blade.ratelimit.path-prefix";
private static final String APPLICATION_NAME_KEY = "spring.application.name";
/**
* 是否微服务部署: 存在 blade-core-cloud 即经网关按服务名路由
*/
private static final boolean MICROSERVICE = ClassUtils.isPresent(
"org.springblade.core.cloud.client.BladeCloudApplication", RateLimitEnvPostProcessor.class.getClassLoader());
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
if (StringUtil.isBlank(environment.getProperty(REDIS_RATE_LIMITER_ENABLED_KEY))) {
PropsUtil.setProperty(System.getProperties(), REDIS_RATE_LIMITER_ENABLED_KEY, StringPool.TRUE);
}
String serviceName = environment.getProperty(APPLICATION_NAME_KEY, StringPool.EMPTY);
if (MICROSERVICE && StringUtil.isNotBlank(serviceName) && StringUtil.isBlank(environment.getProperty(PATH_PREFIX_KEY))) {
PropsUtil.setProperty(System.getProperties(), PATH_PREFIX_KEY, StringPool.SLASH.concat(serviceName));
}
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
@@ -0,0 +1,75 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.ratelimit.props;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 限流配置。
*
* @author Chill
*/
@Data
@ConfigurationProperties("blade.ratelimit")
public class RateLimitProperties {
/**
* 是否开启限流管理
*/
private boolean enabled = false;
/**
* 拦截路径, 默认拦截全部请求
*/
private List<String> includePatterns = new ArrayList<>(Collections.singletonList("/**"));
/**
* 额外的放行路径, 与内置默认排除列表合并后一并放行
*/
private List<String> excludePatterns = new ArrayList<>();
/**
* 规则匹配前缀, 留空则自动推断 (微服务补回网关剥离的 /服务名, 单体为空); 仅当网关前缀异于服务名时才需显式覆盖
*/
private String pathPrefix = "";
/**
* 触发限流时的提示文案
*/
private String rejectMessage = "请求过于频繁,请稍后再试";
/**
* 规则快照刷新间隔, 作为广播失效的兜底, 漏收广播的节点至多在此时长后收敛
*/
private Duration refreshInterval = Duration.ofHours(1);
}