支持用户级别和会话级别接口限流

This commit is contained in:
2023-11-27 15:32:34 +08:00
parent e164fcbf03
commit 58bafb0616
6 changed files with 252 additions and 81 deletions

View File

@@ -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";
}

View File

@@ -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));

View 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();
}
}

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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) {