インフラブログ

とあるWEBサイトのインフラを構築運用するメモ

nginxで同一ユーザからの過度なアクセスを制限する。

連打対策してるのをユーザにわかってもらえる程度の制限

今回運用するWEBサイトではユーザがほぼ同タイミングでPOSTのボタンを数回連打したり、もしくはツールなどを使って一定期間同じリクエストを延々と飛ばしてきたりする可能性が普通にあるらしく、ある程度の規制はいれておきたいと思います。

nginxで使えそうなものがないか色々と調べてみるとここで紹介されているlimit_req_zoneが使えそうです。

下記のように、httpディレクティブで何をキーにしてアクセス数をどの程度で制限するかを定義して、serverのlocation内でどの制限を適用するかを指定します。 制限を超えてアクセスしてきた場合、burst内であればhttp status 499,burstを超えた場合は503として扱い、専用のソーリーページを返すようにします。

http {
    limit_req_zone  $binary_remote_addr  zone=one:10m   rate=1r/s;
   # クライアント側IPをキーにして1秒間に1回までのアクセス上限とする。メモリ10Mを使用してoneという名前でセッション管理する。
    ...
 
    server {
 
        ...
 
        location /search/ {
            limit_req   zone=one  burst=5;
            # /searchには1秒に1リクエストまでの接続が許可される。
            # 1以上のリクエストが来た場合でも5リクエストまでは503エラーとせずに1r/sの割合でリクエストを処理していく。
        }

opensocial_viewer_idをキーにする

クライアント側のIP($binary_remote_addr)をキーにしてアクセスをカウントする例が多いのですが、オープンソーシャルなWEBサイトということもあり、プラットフォームのサーバのIPがクライアントIPになっていたり、携帯端末からアクセスしてきた場合は各キャリアが持つプロキシサーバのIPだったりするので、$binary_remote_addrでユーザを特定できません。

今回の案件ではそのまんまopensocial_viewer_idでユーザを区別することにします。このopensocial_viewer_idが必ずリクエストURLの中に含まれているのであればURLの中からopensocial_viewer_id=〜をすっぱ抜いてlimit_req_zone $opensocial_viewer_id zone=one:10m rate=1r/s;という感じで指定できるのですが、アプリの作りのほうで「そんなパラメータを毎回URLに付けません」ということになっているので、また一工夫必要です。

このRailsアプリではセッション管理にRedisを使用することになっているのですが、このセッションの中にopensocial_viewer_idが保存されていましたので、nginxからこのopensocial_viewer_idを参照することにします。参照する際にセッションキーが必要ですが、これはcookie内の_session_idから取得可能です。

設定抜粋すると下記のようになります。

#参照するredisサーバ
upstream staging_redis {
  server staging-redis-1.example.interal:6379 fail_timeout=0;
  keepalive 1024;
}

server {
...
location / {
    ...
    #cookie内から_session_id抽出
    if ($http_cookie ~ _session_id=([0-9a-f]+)){ 
        set $session $1;
    }
    # _session_idをキーにしてredisサーバからセッション内容を参照して$redisに設定する
    set $redis "-";
    eval $redis {
      #アプリの仕様でredisのindex=2にセッションを格納している
      redis2_query select 2;  
      redis2_query get example:staging:session:$session;
      redis2_pass staging_redis;
    }
    # セッション内容からopensocial_viewer_id部分を抽出して$opensocial_viewer_idに設定する。
    if ($redis ~ opensocial_viewer_id\x06:\x06EFI\x22[\x00-\x1F]([^\;]+)\x06){
      set $opensocial_viewer_id $1;
    }

    # oneというアクセス制限を適用する
    limit_req zone=one burst=2;
    ...
}
...
}

http {
    ...
    # $opensocial_viewer_idをキーとしたアクセス制限を定義する
    limit_req_zone $opensocial_viewer_id zone=one:10m rate=1r/s;
    ...
}

キーが空の場合は

設定した後で気づいたのですが、はじめてサイトに来た場合はsessionが作られてないのでsessionからopensocial_viewer_idを参照できません(空扱い) 。最初に来た人達に対して「キーが空」としてアクセス制限のテーブルを参照してしまうとすぐにアクセス超過となってしまいます。 空の場合の処理を調べてみたところ、キーが空の場合はアクセス制限処理がスルーされるようなので、とりあえず問題は起きなさそうです。