From 58bafb061667aa04e81b6164f113b40db5bc2093 Mon Sep 17 00:00:00 2001 From: lihongjie0209 Date: Mon, 27 Nov 2023 15:32:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=94=A8=E6=88=B7=E7=BA=A7?= =?UTF-8?q?=E5=88=AB=E5=92=8C=E4=BC=9A=E8=AF=9D=E7=BA=A7=E5=88=AB=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E9=99=90=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cn/lihongjie/coal/common/Constants.java | 8 +- .../cn/lihongjie/coal/filter/AuthFilter.java | 5 +- .../coal/filter/RateLimitFilter.java | 116 ++++++++++++ .../coal/session/SessionService.java | 30 +++ .../controller/SysConfigController.java | 2 +- .../sysconfig/service/SysConfigService.java | 172 ++++++++++-------- 6 files changed, 252 insertions(+), 81 deletions(-) create mode 100644 src/main/java/cn/lihongjie/coal/filter/RateLimitFilter.java diff --git a/src/main/java/cn/lihongjie/coal/common/Constants.java b/src/main/java/cn/lihongjie/coal/common/Constants.java index 34de2402..cb411fd7 100644 --- a/src/main/java/cn/lihongjie/coal/common/Constants.java +++ b/src/main/java/cn/lihongjie/coal/common/Constants.java @@ -1,7 +1,5 @@ package cn.lihongjie.coal.common; -import java.util.*; - public class Constants { public static final String CACHE_RESOURCE = "resource"; public static final String CACHE_USER_PERMISSIONS = "userPermissions"; @@ -12,10 +10,16 @@ public class Constants { public static final String CACHE_SYSCONFIG = "sysconfig"; public static final String CACHE_RESOURCE_API_TREE = "resourceApiTree"; public static final String CACHE_RESOURCE_MENU_TREE = "resourceMenuTree"; + public static final String HTTP_HEADER_TOKEN = "X-Token"; + public static final String RATE_LIMIT_GLOBAL_SESSION_PREFIX = "global-session-rl-"; + public static final String RATE_LIMIT_GLOBAL_USER_PREFIX = "global-user-rl-"; public static String SYSCONFIG_ENABLE_CAPTCHA = "enable_captcha"; public static String SYSCONFIG_SESSION_TIMEOUT = "session_timeout"; public static String SYSCONFIG_ACCOUNT_MAX_ONLINE = "account_max_online"; public static String SYSCONFIG_RESETPWD_ENABLE = "resetpwd_enable"; public static String SYSCONFIG_RESETPWD_TIMEOUT = "resetpwd_timeout"; public static String SYSCONFIG_RESETPWD_MAX_FAIL_COUNT = "resetpwd_max_fail_count"; + public static String SYSCONFIG_SESSION_GLOBAL_RATE_LIMIT_PER_MIN = "session_global_rate_limit_per_min"; + public static String SYSCONFIG_USER_GLOBAL_RATE_LIMIT_PER_MIN = "user_global_rate_limit_per_min"; + public static String SYSCONFIG_ANONYMOUS_GLOBAL_RATE_LIMIT_PER_MIN = "anonymous_global_rate_limit_per_min"; } diff --git a/src/main/java/cn/lihongjie/coal/filter/AuthFilter.java b/src/main/java/cn/lihongjie/coal/filter/AuthFilter.java index c79a93f0..93baa968 100644 --- a/src/main/java/cn/lihongjie/coal/filter/AuthFilter.java +++ b/src/main/java/cn/lihongjie/coal/filter/AuthFilter.java @@ -1,6 +1,7 @@ package cn.lihongjie.coal.filter; import cn.lihongjie.coal.base.dto.R; +import cn.lihongjie.coal.common.Constants; import cn.lihongjie.coal.common.Ctx; import cn.lihongjie.coal.exception.BizException; import cn.lihongjie.coal.permission.dto.PermissionDto; @@ -46,7 +47,7 @@ import java.util.Optional; import java.util.function.Consumer; @Component -@Order(0) +@Order(100) public class AuthFilter extends OncePerRequestFilter { @Autowired SessionService sessionService; @@ -102,7 +103,7 @@ public class AuthFilter extends OncePerRequestFilter { return; } - String sessionId = request.getHeader("X-Token"); + String sessionId = request.getHeader(Constants.HTTP_HEADER_TOKEN); Optional resource = resourceService.findUrlFromCache(getRequestURI(request)); diff --git a/src/main/java/cn/lihongjie/coal/filter/RateLimitFilter.java b/src/main/java/cn/lihongjie/coal/filter/RateLimitFilter.java new file mode 100644 index 00000000..4828c51c --- /dev/null +++ b/src/main/java/cn/lihongjie/coal/filter/RateLimitFilter.java @@ -0,0 +1,116 @@ +package cn.lihongjie.coal.filter; + +import cn.lihongjie.coal.base.dto.R; +import cn.lihongjie.coal.common.Constants; +import cn.lihongjie.coal.common.Ctx; +import cn.lihongjie.coal.common.RequestUtils; +import cn.lihongjie.coal.exception.BizException; +import cn.lihongjie.coal.sysconfig.service.SysConfigService; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import org.apache.commons.lang3.StringUtils; +import org.apache.http.entity.ContentType; +import org.redisson.api.RRateLimiter; +import org.redisson.api.RateIntervalUnit; +import org.redisson.api.RateType; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@Order(200) +@Slf4j +public class RateLimitFilter extends OncePerRequestFilter { + + @Autowired SysConfigService sysConfigService; + @Autowired ObjectMapper objectMapper; + @Autowired RedissonClient redissonClient; + + @Override + public void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + String sessionId = request.getHeader(Constants.HTTP_HEADER_TOKEN); + + String ip = RequestUtils.getIp(request); + + if (StringUtils.isAllBlank(ip, sessionId)) { + writeResponse(new BizException("非法请求"), response); + return; + } + + if (StringUtils.isNotEmpty(sessionId)) { + + + RRateLimiter sessionRL = redissonClient.getRateLimiter(Constants.RATE_LIMIT_GLOBAL_SESSION_PREFIX + sessionId); + + boolean acquire = sessionRL.tryAcquire(1); + if (!acquire) { + writeResponse(new BizException("当前会话请求被限流,请稍后再试"), response); + log.warn("会话限流: sessionId {} user {} request {}", sessionId, Ctx.currentUser().getUsername(), request.getRequestURI()); + return; + } + + + + + + RRateLimiter userRL = redissonClient.getRateLimiter(Constants.RATE_LIMIT_GLOBAL_USER_PREFIX + Ctx.getUserId()); + + boolean acquire2 = userRL.tryAcquire(1); + if (!acquire2) { + writeResponse(new BizException("当前用户请求被限流,请稍后再试"), response); + log.warn("用户限流: sessionId {} user {} request {}", sessionId, Ctx.currentUser().getUsername(), request.getRequestURI()); + return; + } + + + }else { + + + RRateLimiter rateLimiter = redissonClient.getRateLimiter("global-iprl-" + ip); + rateLimiter.trySetRate( + RateType.OVERALL, + Integer.parseInt( + sysConfigService.getConfigVal( + Constants.SYSCONFIG_SESSION_GLOBAL_RATE_LIMIT_PER_MIN)), + 1, RateIntervalUnit.MINUTES); + boolean acquire = rateLimiter.tryAcquire(1); + + if (!acquire) { + + writeResponse(new BizException("请求被限流,请稍后再试"), response); + log.warn("ip {} request {} is rate limited", ip, request.getRequestURI()); + return; + } + + + } + + filterChain.doFilter(request, response); + } + + @SneakyThrows + private void writeResponse(BizException ex, HttpServletResponse response) { + + response.setStatus(200); + response.setContentType(ContentType.APPLICATION_JSON.getMimeType()); + R fail = R.fail(ex.getCode(), ex.getMessage()); + response.getOutputStream().write(objectMapper.writeValueAsBytes(fail)); + response.getOutputStream().flush(); + } +} diff --git a/src/main/java/cn/lihongjie/coal/session/SessionService.java b/src/main/java/cn/lihongjie/coal/session/SessionService.java index fe3d28ab..00592dea 100644 --- a/src/main/java/cn/lihongjie/coal/session/SessionService.java +++ b/src/main/java/cn/lihongjie/coal/session/SessionService.java @@ -28,6 +28,10 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.redisson.api.RRateLimiter; +import org.redisson.api.RateIntervalUnit; +import org.redisson.api.RateType; +import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.core.Authentication; @@ -91,6 +95,8 @@ public class SessionService { @Autowired LoginUserHisService loginUserHisService; + @Autowired RedissonClient redissonClient; + @SneakyThrows public void login(LoginDto dto) { HttpServletRequest request = @@ -170,6 +176,30 @@ public class SessionService { his.setLoginTime(LocalDateTime.now()); his.setUser(user); his.setSessionId(sessionId); + + + // 初始化限流 + RRateLimiter sessionRL = redissonClient.getRateLimiter(Constants.RATE_LIMIT_GLOBAL_SESSION_PREFIX + sessionId); + sessionRL.trySetRate( + RateType.OVERALL, + Integer.parseInt( + sysConfigService.getConfigVal( + Constants.SYSCONFIG_SESSION_GLOBAL_RATE_LIMIT_PER_MIN)), + 1, + RateIntervalUnit.MINUTES); + + + RRateLimiter userRL = redissonClient.getRateLimiter(Constants.RATE_LIMIT_GLOBAL_USER_PREFIX + user.getId()); + userRL.trySetRate( + RateType.OVERALL, + Integer.parseInt( + sysConfigService.getConfigVal( + Constants.SYSCONFIG_USER_GLOBAL_RATE_LIMIT_PER_MIN)), + 1, + RateIntervalUnit.MINUTES); + + + loginUserHisService.save(his); } catch (Exception e) { diff --git a/src/main/java/cn/lihongjie/coal/sysconfig/controller/SysConfigController.java b/src/main/java/cn/lihongjie/coal/sysconfig/controller/SysConfigController.java index fe3233ae..d4335159 100644 --- a/src/main/java/cn/lihongjie/coal/sysconfig/controller/SysConfigController.java +++ b/src/main/java/cn/lihongjie/coal/sysconfig/controller/SysConfigController.java @@ -44,7 +44,7 @@ public class SysConfigController extends BaseController { } @PostMapping("/saveAllConfig") - @SysLog(action = "更新系统配置") + @SysLog(action = "更新系统配置", message = "''") public Object saveAllConfig(@RequestBody SaveAllConfigDto dto) { this.service.saveAllConfig(dto); return true; diff --git a/src/main/java/cn/lihongjie/coal/sysconfig/service/SysConfigService.java b/src/main/java/cn/lihongjie/coal/sysconfig/service/SysConfigService.java index 44a4c70f..77dbd4da 100644 --- a/src/main/java/cn/lihongjie/coal/sysconfig/service/SysConfigService.java +++ b/src/main/java/cn/lihongjie/coal/sysconfig/service/SysConfigService.java @@ -41,8 +41,7 @@ import java.util.stream.StreamSupport; @Service @Slf4j @Transactional -public -class SysConfigService extends BaseService { +public class SysConfigService extends BaseService { @Autowired SysConfigRepository repository; @@ -52,93 +51,113 @@ class SysConfigService extends BaseService @PostConstruct public void init() {} + @Autowired CacheManager cacheManager; + public void initDefault() { Map all = StreamSupport.stream(findAll().spliterator(), false) .collect(Collectors.toMap(e -> e.getCode(), e -> e)); - if (!all.containsKey(Constants.SYSCONFIG_ENABLE_CAPTCHA)) { - SysConfigEntity entity = new SysConfigEntity(); - entity.setName("验证码状态"); - entity.setCode(Constants.SYSCONFIG_ENABLE_CAPTCHA); - entity.setConfigVal("1"); - entity.setDictCode("status.type"); - entity.setMaxValue(null); - entity.setMinValue(null); - entity.setRegexValidator(null); - entity.setType("3"); - repository.save(entity); - } + addDictConfig(all, Constants.SYSCONFIG_ENABLE_CAPTCHA, "验证码状态", "1", "status.type"); - if (!all.containsKey(Constants.SYSCONFIG_SESSION_TIMEOUT)) { + addNumberConfig( + all, + Constants.SYSCONFIG_SESSION_TIMEOUT, + "登录会话超时时间(s)", + TimeUnit.HOURS.toSeconds(1) + "", + TimeUnit.MINUTES.toSeconds(1), + TimeUnit.HOURS.toSeconds(24)); + addNumberConfig(all, Constants.SYSCONFIG_ACCOUNT_MAX_ONLINE, "账户同时登录人数", 1 + "", 1L, 100L); + + addDictConfig(all, Constants.SYSCONFIG_RESETPWD_ENABLE, "重置密码状态", "1", "status.type"); + + addNumberConfig( + all, + Constants.SYSCONFIG_RESETPWD_TIMEOUT, + "重置密码会话超时时间(s)", + TimeUnit.MINUTES.toSeconds(10) + "", + TimeUnit.MINUTES.toSeconds(1), + TimeUnit.HOURS.toSeconds(24)); + addNumberConfig( + all, + Constants.SYSCONFIG_RESETPWD_MAX_FAIL_COUNT, + "重置密码最多失败次数", + 3 + "", + 1L, + Integer.MAX_VALUE); + + + addNumberConfig( + all, + Constants.SYSCONFIG_SESSION_GLOBAL_RATE_LIMIT_PER_MIN, + "登录会话全局限流(每分钟)", + 120 + "", + 1L, + Integer.MAX_VALUE); + + + addNumberConfig( + all, + Constants.SYSCONFIG_USER_GLOBAL_RATE_LIMIT_PER_MIN, + "用户全局限流(每分钟)", + 120 + "", + 1L, + Integer.MAX_VALUE); + + + addNumberConfig( + all, + Constants.SYSCONFIG_ANONYMOUS_GLOBAL_RATE_LIMIT_PER_MIN, + "匿名访问全局限流(每分钟)", + 200 + "", + 1L, + Integer.MAX_VALUE); + + + + } + + private void addNumberConfig( + Map all, + String code, + String name, + String value, + Number min, + Number max) { + if (!all.containsKey(code)) { SysConfigEntity entity = new SysConfigEntity(); - entity.setName("登录会话超时时间(s)"); - entity.setCode(Constants.SYSCONFIG_SESSION_TIMEOUT); - entity.setConfigVal(TimeUnit.HOURS.toSeconds(1) + ""); + entity.setName(name); + entity.setCode(code); + entity.setConfigVal(value); entity.setDictCode(null); - entity.setMaxValue((int) TimeUnit.HOURS.toSeconds(24)); - entity.setMinValue((int) TimeUnit.MINUTES.toSeconds(1)); - entity.setRegexValidator(null); - entity.setType("2"); - repository.save(entity); - } - - if (!all.containsKey(Constants.SYSCONFIG_ACCOUNT_MAX_ONLINE)) { - SysConfigEntity entity = new SysConfigEntity(); - entity.setName("账户同时登录人数"); - entity.setCode(Constants.SYSCONFIG_ACCOUNT_MAX_ONLINE); - entity.setConfigVal("1"); - entity.setDictCode(null); - entity.setMaxValue(1000); - entity.setMinValue(1); - entity.setRegexValidator(null); - entity.setType("2"); - repository.save(entity); - } - - if (!all.containsKey(Constants.SYSCONFIG_RESETPWD_ENABLE)) { - SysConfigEntity entity = new SysConfigEntity(); - entity.setName("重置密码状态"); - entity.setCode(Constants.SYSCONFIG_RESETPWD_ENABLE); - entity.setConfigVal("1"); - entity.setDictCode("status.type"); - entity.setMaxValue(null); - entity.setMinValue(null); - entity.setRegexValidator(null); - entity.setType("3"); - repository.save(entity); - } - - if (!all.containsKey(Constants.SYSCONFIG_RESETPWD_TIMEOUT)) { - SysConfigEntity entity = new SysConfigEntity(); - entity.setName("重置密码会话超时时间(s)"); - entity.setCode(Constants.SYSCONFIG_RESETPWD_TIMEOUT); - entity.setConfigVal(TimeUnit.MINUTES.toSeconds(10) + ""); - entity.setDictCode(null); - entity.setMaxValue((int) TimeUnit.HOURS.toSeconds(24)); - entity.setMinValue((int) TimeUnit.MINUTES.toSeconds(1)); - entity.setRegexValidator(null); - entity.setType("2"); - repository.save(entity); - } - - if (!all.containsKey(Constants.SYSCONFIG_RESETPWD_MAX_FAIL_COUNT)) { - SysConfigEntity entity = new SysConfigEntity(); - entity.setName("重置密码最多失败次数"); - entity.setCode(Constants.SYSCONFIG_RESETPWD_MAX_FAIL_COUNT); - entity.setConfigVal(3+""); - entity.setDictCode(null); - entity.setMaxValue(Integer.MAX_VALUE); - entity.setMinValue(1); + entity.setMinValue(min == null ? null : min.intValue()); + entity.setMaxValue(max == null ? null : max.intValue()); entity.setRegexValidator(null); entity.setType("2"); repository.save(entity); } } - @Autowired - CacheManager cacheManager; + private void addDictConfig( + Map all, + String code, + String name, + String value, + String dictCode) { + if (!all.containsKey(code)) { + SysConfigEntity entity = new SysConfigEntity(); + entity.setName(name); + entity.setCode(code); + entity.setConfigVal(value); + entity.setDictCode(dictCode); + entity.setMaxValue(null); + entity.setMinValue(null); + entity.setRegexValidator(null); + entity.setType("3"); + repository.save(entity); + } + } @Autowired ApplicationContext applicationContext; @Cacheable(cacheNames = Constants.CACHE_SYSCONFIG, key = "#configKey") @@ -149,12 +168,13 @@ class SysConfigService extends BaseService return config.getConfigVal(); } - public void clearCache(){ + public void clearCache() { cacheManager.getCache(Constants.CACHE_SYSCONFIG).clear(); } public boolean isEnable(String configKey) { - return StringUtils.equalsIgnoreCase(applicationContext.getBean(this.getClass()).getConfigVal(configKey), "1"); + return StringUtils.equalsIgnoreCase( + applicationContext.getBean(this.getClass()).getConfigVal(configKey), "1"); } public void saveAllConfig(SaveAllConfigDto dto) {