对于那些寻求限制每个用户(IP地址)每秒请求的人,这是一个解决方案。此解决方案需要咖啡因库,该库是Google的java 1.8 +重写。您将使用该类来存储请求计数和客户端 IP 地址。您还需要依赖项,因为您将需要使用请求计数发生的位置。代码如下:Guava library
LoadingCache
javax.servlet-api
servlet filter
import javax.servlet.Filter;
@Component
public class requestThrottleFilter implements Filter {
private int MAX_REQUESTS_PER_SECOND = 5; //or whatever you want it to be
private LoadingCache<String, Integer> requestCountsPerIpAddress;
public requestThrottleFilter(){
super();
requestCountsPerIpAddress = Caffeine.newBuilder().
expireAfterWrite(1, TimeUnit.SECONDS).build(new CacheLoader<String, Integer>() {
public Integer load(String key) {
return 0;
}
});
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
String clientIpAddress = getClientIP((HttpServletRequest) servletRequest);
if(isMaximumRequestsPerSecondExceeded(clientIpAddress)){
httpServletResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
httpServletResponse.getWriter().write("Too many requests");
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
private boolean isMaximumRequestsPerSecondExceeded(String clientIpAddress){
Integer requests = 0;
requests = requestCountsPerIpAddress.get(clientIpAddress);
if(requests != null){
if(requests > MAX_REQUESTS_PER_SECOND) {
requestCountsPerIpAddress.asMap().remove(clientIpAddress);
requestCountsPerIpAddress.put(clientIpAddress, requests);
return true;
}
} else {
requests = 0;
}
requests++;
requestCountsPerIpAddress.put(clientIpAddress, requests);
return false;
}
public String getClientIP(HttpServletRequest request) {
String xfHeader = request.getHeader("X-Forwarded-For");
if (xfHeader == null){
return request.getRemoteAddr();
}
return xfHeader.split(",")[0]; // voor als ie achter een proxy zit
}
@Override
public void destroy() {
}
}
因此,这基本上的作用是将所有发出请求的IP地址存储在.这就像一个特殊的地图,其中每个条目都有一个过期时间。在构造函数中,过期时间设置为 1 秒。这意味着在第一个请求中,IP地址及其请求计数仅在LoadingCache中存储一秒钟。到期时,它会自动从地图中移除。如果在那一秒内,来自IP地址的请求更多,那么会将这些请求添加到总请求计数中,但在此之前检查是否已经超过了每秒的最大请求量。如果是这种情况,它将返回 true,筛选器返回状态代码为 429 的错误响应,该响应代表“请求过多”。LoadingCache
isMaximumRequestsPerSecondExceeded(String clientIpAddress)
这样,每个用户每秒只能发出一定数量的请求。
以下是要添加到您的依赖项Caffeine
pom.xml
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<exclusions>
<exclusion>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
<exclusion>
<artifactId>log4j-over-slf4j</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
请注意该部分。我使用记录器库而不是Spring的默认库。如果您正在 使用,则应从这些 POM 依赖项中删除该部件,否则将不会为此库启用日志记录。<exclusion>
log4j2
logback
logback
<exclusion>
编辑:确保让Spring在您保存过滤器的软件包上进行组件扫描,否则过滤器将无法正常工作。此外,由于它带有@Component因此默认情况下,筛选器将适用于所有终结点 (/*)。
如果 spring 检测到您的过滤器,您应该在启动期间在日志中看到类似内容。
o.s.b.w.servlet.FilterRegistrationBean : Mapping filter:'requestThrottleFilter' to: [/*]
编辑19-01-2022:
我注意到我最初的解决方案在阻止太多请求时有一个缺点,因此我更改了代码。我先解释一下原因。
假设用户每秒可以发出 3 个请求。让我们想象一下,在给定的秒内,用户在该秒的前 200 毫秒内发出第一个请求。这将导致将该用户的条目添加到其中,并且条目将在一秒后自动过期。现在考虑同一个用户仅在第二个经过之前的最后100毫秒内连续发出4个请求,并且该条目被删除。这意味着用户在第四次请求尝试时最多只能被阻止100毫秒。在这100毫秒过去后,他将能够立即提出三个新请求。requestCountsPerIpAddress
因此,他还能够在一秒钟内提出5个请求,而不是3个。当第一个请求(在 中创建条目)和接下来的两个请求(均在当前条目过期前的最后 500 毫秒内发出)之间至少存在 500 毫秒的延迟时,可能会发生这种情况。如果用户在条目过期后立即发出3个请求,他将有效地设法在1秒的时间跨度内发出5个请求,而只允许3个请求(2个在前一个条目过期前的最后500毫秒内发出+3个在新条目的前500ms内发出)。因此,这不是限制请求的非常有效的方法。LoadingCache
我已将库更改为咖啡因,因为番石榴库存在一些死锁问题。如果你想继续使用番石榴库本身,你应该在代码下面添加这一行。这基本上可以删除IP地址的当前条目。然后在下一行上再次添加它,这会将该条目的到期时间重置为整整一秒。requestCountsPerIpAddress.asMap().remove(clientIpAddress);
if(requests > MAX_REQUESTS_PER_SECOND) {
这样做的效果是,任何只是不断向 REST 终结点发送请求的人都会无限期地收到 409 响应,直到用户在最后一个请求后停止发送请求一秒钟。