Spring Boot で各ユーザーのレート制限を設定するにはどうすればいいですか? 質問する

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

そこで、各ユーザーにレート制限を設定したいと考えました。各ユーザーは 1 分あたり 5 件のリクエストしかリクエストできない、などとします。各ユーザーのレート制限を設定して 1 分あたり 5 件の API 呼び出しのみを行い、ユーザーがそれ以上のリクエストをした場合は 429 応答を返すようにするにはどうすればよいでしょうか。ユーザーの IP アドレスは必要ですか。

どのような助けでも大歓迎です。

ベストアンサー1

ここでは、各ユーザー(IPアドレス)の1秒あたりのリクエスト数を制限したい人向けのソリューションを紹介します。このソリューションでは、カフェインライブラリこれはJava 1.8+ の書き換えGoogle の です。リクエスト数とクライアント IP アドレスを保存するために クラスGuava libraryを使用します。リクエストのカウントが行われる を使用する必要があるため、依存関係も必要になります。コードは次のとおりです。LoadingCachejavax.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 アドレスを に格納しますLoadingCache。これは、各エントリに有効期限がある特別なマップのようなものです。コンストラクターでは、有効期限は 1 秒に設定されています。つまり、最初のリクエストでは、IP アドレスとそのリクエスト数が LoadingCache に 1 秒間だけ格納されます。有効期限が切れると、マップから自動的に削除されます。その 1 秒間にその IP アドレスからさらにリクエストが送信された場合、 はisMaximumRequestsPerSecondExceeded(String clientIpAddress)それらのリクエストを合計リクエスト数に追加しますが、その前に、1 秒あたりの最大リクエスト量をすでに超えていないかどうかを確認します。超えている場合は true を返し、フィルターはステータスコード 429 (リクエストが多すぎる) のエラー応答を返します。

この方法では、ユーザーごとに 1 秒あたり設定された量のリクエストのみが実行されます。

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のデフォルトライブラリの代わりにロガーライブラリとして<exclusion>使用しています。log4j2logbackを使用する場合logbackは、これらの POM 依存関係から部分を削除する必要があります<exclusion>。そうしないと、このライブラリのログ記録が有効になりません。

編集: フィルターを保存したパッケージで Spring がコンポーネント スキャンを実行するようにしてください。そうしないと、フィルターは機能しません。また、@Component で注釈が付けられているため、フィルターはデフォルトですべてのエンドポイントで機能します (/*)。

Spring がフィルターを検出した場合、起動時にログに次のような内容が表示されます。

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

編集 2022-01-19:

私の最初の解決策には、ブロックされるリクエストが多すぎるという欠点があることに気づいたので、コードを変更しました。まずその理由を説明します。

ユーザーは 1 秒あたり 3 つのリクエストを実行できるとします。ある 1 秒以内に、ユーザーが最初のリクエストをその秒の最初の 200 ミリ秒の間に実行したとします。これにより、そのユーザーのエントリが追加されrequestCountsPerIpAddress、エントリは 1 秒後に自動的に期限切れになります。次に、この同じユーザーが最後の 100 ミリ秒の間に 4 つのリクエストを連続して実行し、1 秒が経過してエントリが削除されるとします。つまり、ユーザーは 4 回目のリクエスト試行で最大 100 ミリ秒だけブロックされることになります。この 100 ミリ秒が経過すると、ユーザーはすぐに 3 つの新しいリクエストを実行できるようになります。

この結果、1 秒以内に 3 つではなく 5 つのリクエストを行うこともできます。これは、最初のリクエスト ( にエントリを作成するLoadingCache) と次の 2 つのリクエスト (どちらも現在のエントリの有効期限が切れる前の最後の 500 ミリ秒以内に行われたもの) の間に 500 ミリ秒以上の遅延がある場合に発生する可能性があります。エントリの有効期限が切れた直後にユーザーが 3 つのリクエストを行った場合、1 秒の期間内に 5 つのリクエストを行うことができますが、許可されるのは 3 つだけです (前のエントリの有効期限が切れる前の最後の 500 ミリ秒以内に行われた 2 つ + 新しいエントリの最初の 500 ミリ秒以内に行われた 3 つ)。したがって、これはリクエストを調整するあまり効率的な方法ではありません。

guava ライブラリでデッドロックの問題が発生するため、ライブラリを caffeine に変更しました。guava ライブラリ自体を引き続き使用したい場合は、コードのrequestCountsPerIpAddress.asMap().remove(clientIpAddress);すぐ下にこの行を追加する必要がありますif(requests > MAX_REQUESTS_PER_SECOND) {。これは基本的に、IP アドレスの現在のエントリを削除します。次の行で再度追加され、そのエントリの有効期限が 1 秒にリセットされます。

これにより、REST エンドポイントにリクエストをスパムし続けると、ユーザーが最後のリクエストから 1 秒間リクエストの送信を停止するまで、無期限に 409 応答が返されることになります。

おすすめ記事