mirror of
https://codeup.aliyun.com/64f7d6b8ce01efaafef1e678/coal/coal.git
synced 2026-01-25 07:46:40 +08:00
支持用户级别和会话级别接口限流
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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<ResourceDto> resource =
|
||||
resourceService.findUrlFromCache(getRequestURI(request));
|
||||
|
||||
116
src/main/java/cn/lihongjie/coal/filter/RateLimitFilter.java
Normal file
116
src/main/java/cn/lihongjie/coal/filter/RateLimitFilter.java
Normal file
@@ -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<Object> fail = R.fail(ex.getCode(), ex.getMessage());
|
||||
response.getOutputStream().write(objectMapper.writeValueAsBytes(fail));
|
||||
response.getOutputStream().flush();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -41,8 +41,7 @@ import java.util.stream.StreamSupport;
|
||||
@Service
|
||||
@Slf4j
|
||||
@Transactional
|
||||
public
|
||||
class SysConfigService extends BaseService<SysConfigEntity, SysConfigRepository> {
|
||||
public class SysConfigService extends BaseService<SysConfigEntity, SysConfigRepository> {
|
||||
|
||||
@Autowired SysConfigRepository repository;
|
||||
|
||||
@@ -52,93 +51,113 @@ class SysConfigService extends BaseService<SysConfigEntity, SysConfigRepository>
|
||||
@PostConstruct
|
||||
public void init() {}
|
||||
|
||||
@Autowired CacheManager cacheManager;
|
||||
|
||||
public void initDefault() {
|
||||
|
||||
Map<String, SysConfigEntity> 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<String, SysConfigEntity> 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<String, SysConfigEntity> 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<SysConfigEntity, SysConfigRepository>
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user