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

新手上路,请多包涵

我正在开发一个处理大量传入请求调用的 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 地址吗?

任何帮助表示赞赏。

原文由 Ricky 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 824
2 个回答

你在 Spring 中没有那个组件。

  • 您可以将其构建为解决方案的一部分。创建一个过滤器并将其注册到您的 spring 上下文中。过滤器应检查传入呼叫并计算每个用户在时间窗口内的传入请求。我会使用 令牌桶算法,因为它是最灵活的。
  • 您可以构建一些独立于当前解决方案的组件。创建一个 API 网关来完成这项工作。您可以扩展 Zuul 网关,并再次使用令牌桶算法。
  • 您可以使用已经内置的组件,例如 Mulesoft ESB,它可以充当 API 网关并支持速率限制和节流。我自己从来没用过。
  • 最后,您可以使用具有速率限制和节流等功能的 API 管理器。检查 MuleSoft、WSO2、3Scale、Kong 等……(大多数都需要付费,有些是开源的,并且有社区版)。

原文由 Daniel Cerecedo 发布,翻译遵循 CC BY-SA 4.0 许可协议

对于那些试图限制每个用户(IP 地址)每秒请求的人来说,这是一个解决方案。此解决方案需要 Caffeine 库,它是 Google 的 Guava libraryjava 1.8+ 重写。您将使用 LoadingCache 类来存储请求计数和客户端 ip 地址。您还需要 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 地址存储在 LoadingCache 中。这就像一个特殊的地图,其中每个条目都有一个过期时间。在构造函数中,过期时间设置为 1 秒。这意味着在第一次请求时,一个 ip 地址及其请求计数仅在 LoadingCache 中存储一秒钟。它会在到期时自动从地图中删除。如果在那一秒内有更多请求来自该 ip 地址,则 isMaximumRequestsPerSecondExceeded(String clientIpAddress) 会将这些请求添加到总请求计数中,但在此之前检查是否已超过每秒最大请求量。如果是这种情况,它会返回 true,过滤器会返回一个错误响应,状态码为 429,表示请求过多。

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

这是要添加到您的 --- 的 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>

请注意 <exclusion> 部分。我正在使用 log4j2 作为记录器库,而不是 Spring 的默认 logback 库。如果您 正在 使用 logback 那么您应该从这些 POM 依赖项中删除 <exclusion> 部分,否则将不会为此库启用日志记录。

编辑:确保让 Spring 对保存过滤器的包进行组件扫描,否则过滤器将无法工作。此外,因为它带有 @Component 注释,所以默认情况下过滤器将适用于所有端点 (/*)。

如果 spring 检测到您的过滤器,您应该在启动期间在日志中看到类似这样的内容。

o.s.b.w.servlet.FilterRegistrationBean : Mapping filter:'requestThrottleFilter' to: [/*]

编辑 19-01-2022:

我注意到我最初的解决方案在阻止太多请求时有一个缺点,因此我更改了代码。我会先解释为什么。

考虑一个用户每秒可以发出 3 个请求。假设在给定的一秒内,用户在该秒的前 200 毫秒内发出第一个请求。这会导致该用户的条目被添加到 requestCountsPerIpAddress 并且条目将在一秒后自动过期。现在考虑同一用户仅在最后 100 毫秒内发出 4 次连续请求,然后第二次过去,条目被删除。这意味着用户在第四次请求尝试时最多只会被阻塞 100 毫秒。在这 100 毫秒过去之后,他将能够立即发出三个新请求。

因此,他还能够在一秒内发出 5 个请求,而不是 3 个。当第一个请求之间至少有 500 毫秒的延迟时,就会发生这种情况(这会在 LoadingCache 中创建条目)以及接下来的两个请求(均在当前条目到期前的最后 500 毫秒内发出)。如果用户在条目过期后立即发出 3 个请求,他将有效地在 1 秒的时间跨度内发出 5 个请求,而只允许 3 个(2 个在前一个条目过期前的最后 500 毫秒内发出 + 3 个在此期间发出新的第一个 500 毫秒)。所以这不是一种非常有效的限制请求的方法。

我已将库更改为咖啡因,因为番石榴库存在一些死锁问题。如果你想继续使用 guava 库本身,你应该在代码中的 if(requests > MAX_REQUESTS_PER_SECOND) { requestCountsPerIpAddress.asMap().remove(clientIpAddress); 。这基本上是删除 ip 地址的当前条目。然后在下一行再次添加它,这会将该条目的到期时间重置为一整秒。

这会产生这样的效果,即任何不断向 REST 端点发送请求的人都将无限期地收到 409 响应,直到用户在他的最后一个请求后停止发送请求一秒钟。

原文由 Maurice 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题