From faa5f1a9a898195bb434a5d452952d2f54038bd0 Mon Sep 17 00:00:00 2001 From: Theo <971366405@qq.com> Date: Sat, 10 Aug 2024 15:01:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0ip=E9=99=90=E6=B5=81?= =?UTF-8?q?=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用系统配置增加ip限流控制 --- sql/mysql5/youlai_boot.sql | 3 + sql/mysql8/youlai_boot.sql | 3 + .../common/constant/RedisKeyConstants.java | 10 +++ .../common/constant/SystemConstants.java | 12 +++ .../system/common/result/ResultCode.java | 4 +- .../youlai/system/config/SecurityConfig.java | 6 +- .../system/filter/RedisRateLimiterFilter.java | 89 +++++++++++++++++++ .../aspect/DuplicateSubmitAspect.java | 4 +- .../service/impl/SysConfigServiceImpl.java | 11 ++- 9 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/youlai/system/filter/RedisRateLimiterFilter.java diff --git a/sql/mysql5/youlai_boot.sql b/sql/mysql5/youlai_boot.sql index 1dbf35ee..0911e89f 100644 --- a/sql/mysql5/youlai_boot.sql +++ b/sql/mysql5/youlai_boot.sql @@ -37,6 +37,9 @@ CREATE TABLE `sys_config` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB COMMENT='系统配置'; +INSERT INTO `sys_config` VALUES(1,'IP限流控制单位时长','IP_RATE_LIMIT_MINUTE','1','IP限流控制单位时长(分钟)','2024-08-10 14:31:34','2','2024-08-10 14:53:51','2',0); +INSERT INTO `sys_config` VALUES(2,'IP限流控制最大访问次数配置','IP_RATE_LIMIT_COUNT','100','IP限流控制一个单位时长内最大访问次数','2024-08-10 14:31:51','2','2024-08-10 14:53:09','2',0); + -- ---------------------------- -- Table structure for sys_dept -- ---------------------------- diff --git a/sql/mysql8/youlai_boot.sql b/sql/mysql8/youlai_boot.sql index b8818bc9..c587e890 100644 --- a/sql/mysql8/youlai_boot.sql +++ b/sql/mysql8/youlai_boot.sql @@ -38,6 +38,9 @@ CREATE TABLE `sys_config` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB COMMENT='系统配置'; +INSERT INTO `sys_config` VALUES(1,'IP限流控制单位时长','IP_RATE_LIMIT_MINUTE','1','IP限流控制单位时长(分钟)','2024-08-10 14:31:34','2','2024-08-10 14:53:51','2',0); +INSERT INTO `sys_config` VALUES(2,'IP限流控制最大访问次数配置','IP_RATE_LIMIT_COUNT','100','IP限流控制一个单位时长内最大访问次数','2024-08-10 14:31:51','2','2024-08-10 14:53:09','2',0); + -- ---------------------------- -- Table structure for sys_dept -- ---------------------------- diff --git a/src/main/java/com/youlai/system/common/constant/RedisKeyConstants.java b/src/main/java/com/youlai/system/common/constant/RedisKeyConstants.java index cba3064c..f26f3b40 100644 --- a/src/main/java/com/youlai/system/common/constant/RedisKeyConstants.java +++ b/src/main/java/com/youlai/system/common/constant/RedisKeyConstants.java @@ -12,4 +12,14 @@ public interface RedisKeyConstants { * 系统配置Redis-key */ String SYSTEM_CONFIG_KEY = "system:config"; + + /** + * IP限流Redis-key + */ + String IP_RATE_LIMITER_KEY = "ip:rate:limiter:"; + + /** + * 防重复提交Redis-key + */ + String RESUBMIT_LOCK_PREFIX = "resubmit:lock:"; } diff --git a/src/main/java/com/youlai/system/common/constant/SystemConstants.java b/src/main/java/com/youlai/system/common/constant/SystemConstants.java index 4eca3ee3..7c9ba50e 100644 --- a/src/main/java/com/youlai/system/common/constant/SystemConstants.java +++ b/src/main/java/com/youlai/system/common/constant/SystemConstants.java @@ -23,4 +23,16 @@ public interface SystemConstants { */ String ROOT_ROLE_CODE = "ROOT"; + /** + * IP限流最大分钟数配置系统配置KEY + */ + String CONFIG_IP_RATE_LIMIT_MINUTE_KEY = "IP_RATE_LIMIT_MINUTE"; + + /** + * IP限流次数配置系统配置KEY + * 在最大分钟数内,允许访问的次数 + * @since 1.0.0 + */ + String CONFIG_IP_RATE_LIMIT_COUNT_KEY = "IP_RATE_LIMIT_COUNT"; + } diff --git a/src/main/java/com/youlai/system/common/result/ResultCode.java b/src/main/java/com/youlai/system/common/result/ResultCode.java index 4e6a64ea..09b03fd2 100644 --- a/src/main/java/com/youlai/system/common/result/ResultCode.java +++ b/src/main/java/com/youlai/system/common/result/ResultCode.java @@ -56,8 +56,8 @@ public enum ResultCode implements IResultCode, Serializable { SYSTEM_EXECUTION_TIMEOUT("B0100", "系统执行超时"), SYSTEM_ORDER_PROCESSING_TIMEOUT("B0100", "系统订单处理超时"), - SYSTEM_DISASTER_RECOVERY_TRIGGER("B0200", "系统容灾功能被出发"), - FLOW_LIMITING("B0210", "系统限流"), + SYSTEM_DISASTER_RECOVERY_TRIGGER("B0200", "系统容灾功能被触发"), + FLOW_LIMITING("B0210", "系统限流,请稍后再试"), DEGRADATION("B0220", "系统功能降级"), SYSTEM_RESOURCE_ERROR("B0300", "系统资源异常"), diff --git a/src/main/java/com/youlai/system/config/SecurityConfig.java b/src/main/java/com/youlai/system/config/SecurityConfig.java index 13adf7a4..2aa483d2 100644 --- a/src/main/java/com/youlai/system/config/SecurityConfig.java +++ b/src/main/java/com/youlai/system/config/SecurityConfig.java @@ -4,10 +4,12 @@ import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.core.collection.CollectionUtil; import com.youlai.system.common.constant.SecurityConstants; import com.youlai.system.config.property.SecurityProperties; +import com.youlai.system.filter.RedisRateLimiterFilter; import com.youlai.system.security.exception.MyAccessDeniedHandler; import com.youlai.system.security.exception.MyAuthenticationEntryPoint; import com.youlai.system.filter.JwtValidationFilter; import com.youlai.system.filter.CaptchaValidationFilter; +import com.youlai.system.service.SysConfigService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -43,6 +45,7 @@ public class SecurityConfig { private final RedisTemplate redisTemplate; private final CodeGenerator codeGenerator; private final SecurityProperties securityProperties; + private final SysConfigService sysConfigService; @@ -64,7 +67,8 @@ public class SecurityConfig { .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) ; - + // 限流过滤器 + http.addFilterBefore(new RedisRateLimiterFilter(redisTemplate, sysConfigService), UsernamePasswordAuthenticationFilter.class); // 验证码校验过滤器 http.addFilterBefore(new CaptchaValidationFilter(redisTemplate, codeGenerator), UsernamePasswordAuthenticationFilter.class); // JWT 校验过滤器 diff --git a/src/main/java/com/youlai/system/filter/RedisRateLimiterFilter.java b/src/main/java/com/youlai/system/filter/RedisRateLimiterFilter.java new file mode 100644 index 00000000..4d34c3c7 --- /dev/null +++ b/src/main/java/com/youlai/system/filter/RedisRateLimiterFilter.java @@ -0,0 +1,89 @@ +package com.youlai.system.filter; + +import com.youlai.system.common.constant.RedisKeyConstants; +import com.youlai.system.common.constant.SystemConstants; +import com.youlai.system.common.result.ResultCode; +import com.youlai.system.service.SysConfigService; +import com.youlai.system.util.IPUtils; +import com.youlai.system.util.ResponseUtils; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * IP限流过滤器 + * + * @author Theo + * @since 2024/08/10 14:38 + */ +@Slf4j +public class RedisRateLimiterFilter extends OncePerRequestFilter { + + private final RedisTemplate redisTemplate; + private final SysConfigService sysConfigService; + + public RedisRateLimiterFilter(RedisTemplate redisTemplate, SysConfigService sysConfigService) { + this.redisTemplate = redisTemplate; + this.sysConfigService = sysConfigService; + } + + /** + * 确认是否限流方法 + * 默认情况下:限制同一个IP在一分钟内只能访问10次,可以通过修改系统配置进行调整 + * 这里也可以进行扩展,比如redis记录同一个ip每天出发限流的上限次数,记录在redis中,达到某个阈值后,进行永久封禁这· + * + * @param ip ip地址 + * @return 是否限流 + */ + public boolean rateLimit(String ip) { + String key = RedisKeyConstants.IP_RATE_LIMITER_KEY + ip; + Long count = redisTemplate.opsForValue().increment(key); + if (count == null || count == 1) { + Object ipRateLimitMinute = sysConfigService.getSystemConfig(SystemConstants.CONFIG_IP_RATE_LIMIT_MINUTE_KEY); + long expire = 1; + if(ipRateLimitMinute != null){ + expire = Long.parseLong(ipRateLimitMinute.toString()); + }else { + log.warn("[RedisRateLimiterFilter.rateLimit]系统配置中未配置IP限流最大分钟数,使用默认分钟数:{},请检查配置项:{}", + expire,SystemConstants.CONFIG_IP_RATE_LIMIT_MINUTE_KEY); + } + redisTemplate.expire(key,expire, TimeUnit.MINUTES); + } + Object systemConfig = sysConfigService.getSystemConfig(SystemConstants.CONFIG_IP_RATE_LIMIT_COUNT_KEY); + long limit = 10; + if(systemConfig != null){ + limit = Long.parseLong(systemConfig.toString()); + }else{ + log.warn("[RedisRateLimiterFilter.rateLimit]系统配置中未配置IP限流次数配置,使用默认次数:{},请检查配置项:{}", + limit,SystemConstants.CONFIG_IP_RATE_LIMIT_COUNT_KEY); + } + return count != null && count > limit; + } + + /** + * IP限流过滤器 + * 默认情况下:限制同一个IP在一分钟内只能访问10次,可以通过修改系统配置进行调整 + * + * @param request 请求体 + * @param response 响应体 + * @param filterChain 过滤器链 + */ + @Override + protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, + @NotNull FilterChain filterChain) throws ServletException, IOException { + String ip = IPUtils.getIpAddr(request); + if (rateLimit(ip)) { + ResponseUtils.writeErrMsg(response, ResultCode.FLOW_LIMITING); + return; + } + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/youlai/system/plugin/norepeat/aspect/DuplicateSubmitAspect.java b/src/main/java/com/youlai/system/plugin/norepeat/aspect/DuplicateSubmitAspect.java index 50ef84ac..eb1f0b32 100644 --- a/src/main/java/com/youlai/system/plugin/norepeat/aspect/DuplicateSubmitAspect.java +++ b/src/main/java/com/youlai/system/plugin/norepeat/aspect/DuplicateSubmitAspect.java @@ -3,6 +3,7 @@ package com.youlai.system.plugin.norepeat.aspect; import cn.hutool.core.util.StrUtil; import cn.hutool.jwt.JWTUtil; import cn.hutool.jwt.RegisteredPayload; +import com.youlai.system.common.constant.RedisKeyConstants; import com.youlai.system.common.constant.SecurityConstants; import com.youlai.system.exception.BusinessException; import com.youlai.system.common.result.ResultCode; @@ -36,7 +37,6 @@ import java.util.concurrent.TimeUnit; public class DuplicateSubmitAspect { private final RedissonClient redissonClient; - private static final String RESUBMIT_LOCK_PREFIX = "LOCK:RESUBMIT:"; /** * 防重复提交切点 @@ -74,7 +74,7 @@ public class DuplicateSubmitAspect { token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); // 从 JWT Token 中获取 jti String jti = (String) JWTUtil.parseToken(token).getPayload(RegisteredPayload.JWT_ID); - resubmitLockKey = RESUBMIT_LOCK_PREFIX + jti + ":" + request.getMethod() + "-" + request.getRequestURI(); + resubmitLockKey = RedisKeyConstants.RESUBMIT_LOCK_PREFIX + jti + ":" + request.getMethod() + "-" + request.getRequestURI(); } return resubmitLockKey; } diff --git a/src/main/java/com/youlai/system/service/impl/SysConfigServiceImpl.java b/src/main/java/com/youlai/system/service/impl/SysConfigServiceImpl.java index e0e1c8c1..ca6be5b5 100644 --- a/src/main/java/com/youlai/system/service/impl/SysConfigServiceImpl.java +++ b/src/main/java/com/youlai/system/service/impl/SysConfigServiceImpl.java @@ -1,7 +1,6 @@ package com.youlai.system.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; @@ -10,6 +9,7 @@ import com.youlai.system.converter.SysConfigConverter; import com.youlai.system.model.form.ConfigForm; import com.youlai.system.model.query.ConfigPageQuery; import com.youlai.system.security.util.SecurityUtils; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springframework.data.redis.core.RedisTemplate; @@ -40,6 +40,15 @@ public class SysConfigServiceImpl extends ServiceImpl redisTemplate; + + /** + * 系统启动完成后,加载系统配置到缓存 + */ + @PostConstruct + public void init() { + refreshCache(); + } + /** * 分页查询系统配置 *