如何在Spring Boot中为每个用户设置速率限制?

2022-09-01 04:18:36

我正在开发一个Spring Boot Rest API,它可以处理大量的传入请求调用。我的控制器如下所示:

@RestController

public class ApiController {
    List<ApiObject>  apiDataList;   

    @RequestMapping(value="/data",produces={MediaType.APPLICATION_JSON_VALUE},method=RequestMethod.GET)
    public ResponseEntity<List<ApiObject>> getData(){                                       
        List<ApiObject> apiDataList=getApiData();
        return new ResponseEntity<List<ApiObject>>(apiDataList,HttpStatus.OK);
    }
    @ResponseBody 
    @Async  
    public List<ApiObject>  getApiData(){
        List<ApiObject>  apiDataList3=new List<ApiObject> ();
        //do the processing
        return apiDataList3;
    }
}

所以现在我想为每个用户设置一个速率限制。假设每个用户每分钟只能请求5个请求或类似的东西。如何为每个用户设置速率限制,使其每分钟仅进行 5 次 api 调用,如果用户请求超过此值,我可以发回 429 响应?我们需要他们的IP地址吗?

任何帮助是值得赞赏的。


答案 1

对于那些寻求限制每个用户(IP地址)每秒请求的人,这是一个解决方案。此解决方案需要咖啡因库,该库是Google的java 1.8 +重写。您将使用该类来存储请求计数和客户端 IP 地址。您还需要依赖项,因为您将需要使用请求计数发生的位置。代码如下:Guava libraryLoadingCachejavax.servlet-apiservlet 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 的错误响应,该响应代表“请求过多”。LoadingCacheisMaximumRequestsPerSecondExceeded(String clientIpAddress)

这样,每个用户每秒只能发出一定数量的请求。

以下是要添加到您的依赖项Caffeinepom.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>log4j2logbacklogback<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 响应,直到用户在最后一个请求后停止发送请求一秒钟。


答案 2

你在春天没有这个组件。

  • 可以将其作为解决方案的一部分进行生成。创建一个过滤器并将其注册到您的春季环境中。筛选器应检查传入呼叫,并计算某个时间范围内每个用户的传入请求数。我会使用令牌桶算法,因为它是最灵活的。
  • 您可以生成一些独立于当前解决方案的组件。创建用于完成这项工作的 API 网关。您可以扩展 Zuul 网关,并再次使用令牌存储桶算法。
  • 您可以使用已经内置的组件,例如Mulesoft ESB,它可以充当API网关并支持速率限制和限制。我自己从来没有用过它。
  • 最后,您可以使用具有速率限制和节流等功能的 API 管理器。结帐 MuleSoft, WSO2, 3Scale, Kong, 等等...(大多数都有成本,有些是开源的,有社区版)。