会員制サイトで、ローカルキャッシュとサーブレットフィルタを使ってF5アタック対策

以前、このような記事を書きましたが、

 * **[logmonとALBのdrainingでリロード攻撃（F5アタック）へのパッシブ対策](https://qiita.com/hmatsu47/items/15ba092a2e27d5319799)**

Tomcatを再起動する事態になる前に、それをできるだけ食い止めるための対策が今回のネタです。

# 1. 前提

まず、Tomcatより前（クライアント寄り）の段階で、WAFやNginx、ApacheなどのDoSフィルタで「異常な数のリクエスト」は適切に遮断してあることが前提となります。

また、今回の対策は、

 * **正規のユーザ（Cookieで識別可能なユーザ）が『遅い』処理に対して過剰なリクエストを投げてきたときに、一定時間リクエストを遮断する**

という内容ですので、正規ユーザ以外の不正なDoSリクエストを防ぐものではない点にご注意ください（一部は正規ユーザと同様遮断されますが、確実に遮断するものではありません）。

※Webブラウザでユーザの過剰操作を直接抑制する対策も含まれていません。
そちらは別途対応してください。

# 2. 内容

**遅い処理に対してだけ**、サーブレットフィルタを適用して**「同一ユーザの、同時並行している遅い処理」の数をカウント**し、それが一定数を超えた場合に**「遮断時間」をセットし、エラー画面へ遷移**させるものです。

処理終了でカウントを減らすために、サーブレットフィルタの後処理を使うのがポイントです。

そうすることによって、ユーザや対象データなどによって処理時間にばらつきがある場合でも、リクエストの遮断が有効に働きます。

※すべての処理に適用するのはお勧めしません。

 * 同一ユーザの判定にはCookieの値（TomcatのデフォルトのセッションIDであればJSESSIONID）を使う
 * カウンタや遮断時間のタイマとして、**[Caffeine](https://github.com/ben-manes/caffeine)**を使う
 * 遮断時間中に重ねてリクエストが届いた場合、遮断時間を延長する

ユーザ識別用のCookie（JSESSIONID）が付かないリクエストは全て「1人分のリクエスト」としてまとめてカウントします。
なお、Caffeineについては、昨年のJava Advent Calendar（4日目）にて、Kazuhiraさんが紹介記事を書かれています。

 * **[Java Advent Calendar 2016](https://qiita.com/advent-calendar/2016/java)**
 * **[ローカルキャッシュにCaffeineでも](http://d.hatena.ne.jp/Kazuhira/20161204/1480818544)（CLOVER／Kazuhiraさん）**

# 3. コード

フィルタ用のクラスと、そこから呼び出すユーティリティクラスを作りました。
キャッシュの数や保持時間等は適宜調整してください。
また、ここではアノテーションでフィルタ対象のサーブレットクラスを指定していますが、web.xmlで指定しても構いません。
なお、コードにはログ記録処理が含まれていませんが（動作確認用の出力のためのsystem.out.println()がコメントアウトされていますが、これを使うのではなく、このフィルタを組み込むシステムに合った）適切なコードを追加してきちんとログ出力しましょう。

```java:F5AttackFilter.java
package f5test;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;

/**
 * ***************************************************<BR>
 * フィルタ <BR>
 * ***************************************************<BR>
 * F5攻撃を緩和するフィルタのクラスです。<BR>
 */

// フィルタ対象を指定してください
@WebFilter(filterName="f5-filter", urlPatterns={"/Test2Wait1Sec", "/Test3Wait4Sec"})
public class F5AttackFilter implements Filter {


	// エラー画面
	private static final String ERROR_PAGE = "/error.html";

	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {

		// CookieからセッションIDを取得→値がなければ「-」で置き換え
		//（セッションIDなしが連続で来たら遮断するため）
		// 但し、ログイン処理の実装によっては正規ユーザがログインできない
		// ようにするDoS攻撃の対象になりうるため、その場合は「.orElse()」
		// の内容をランダムなものに変更すべき（IPアドレス＋乱数など）
		final String sid = F5AttackUtil.getSessionId(request).orElse("-");
		if((F5AttackUtil.getRejectTimeCache(sid)).isPresent()) {
			// 遮断中のリクエスト→遮断時間を延長してエラーページへ
//			System.out.println("SID:" + sid + " Reject!");
			setForwardToAlert(request, response);
			return;
		};
		// カウントアップ→閾値チェック
		final Integer attackCount = F5AttackUtil.incAttackCount(sid, 1);
		System.out.println("SID:" + sid + " Count:" + attackCount);
		if(F5AttackUtil.ATTACK_COUNT_LIMIT <= attackCount) {
			// カウントオーバー→遮断時間を設定してエラーページへ
			F5AttackUtil.setRejectTimeCache(sid, 1);
			setForwardToAlert(request, response);
			return;
		}
		// 次のフィルタ or サーブレット本体の処理へ
		try {
			chain.doFilter(request, response);
		} finally {
			// 処理完了時にキャッシュエントリが残っていれば攻撃カウントをデクリメント
			F5AttackUtil.incAttackCount(sid, -1);
//			final Integer attackCountDec = F5AttackUtil.incAttackCount(sid, -1);
//			System.out.println("SID:" + sid + " Count:" + attackCountDec);
		}
	}

	public void init(FilterConfig filterConfig) throws ServletException {
	}

	public void destroy() {
	}

	/**
	 * 閾値越え→遷移先を警告画面にします
	 * @param request
	 * @param response
	 */
	public static void setForwardToAlert(ServletRequest request, ServletResponse response)
			throws IOException, ServletException {
		request.getRequestDispatcher(ERROR_PAGE).forward(request, response);
	}
}
```

```java:F5AttackUtil.java
package f5test;

import java.util.Arrays;
import java.util.Optional;

import javax.servlet.ServletRequest;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

public class F5AttackUtil {

	/**
	 * ***************************************************<BR>
	 * F5攻撃対策 <BR>
	 * ***************************************************<BR>
	 * F5攻撃対策のユーティリティクラスです。<BR>
	 */

	// セッションID名
	private static final String SESSION_ID_NAME			= "SID";  // JSESSIONID等にすると良い

	// F5攻撃カウント／遮断の閾値
	public static final int ATTACK_COUNT_LIMIT			= 5; // 閾値+1を入れる

	// F5攻撃カウント用キャッシュの設定
	private static final String ATTACK_LIMIT_SETTING		= "initialCapacity=100,maximumSize=1000,expireAfterAccess=15s"; // 保持数1,000個・保持時間15秒
	// 遮断タイマー用キャッシュの設定
	private static final String REJECT_TIME_SETTING		= "initialCapacity=10,maximumSize=100,expireAfterAccess=30s"; // 保持数100個・保持（遮断）時間30秒

	// F5攻撃カウント用キャッシュ
	private static Cache<String, Integer> attackLimitCache	= Caffeine.from(ATTACK_LIMIT_SETTING).build();
	// 遮断タイマー用キャッシュ
	private static Cache<String, Integer> rejectTimeCache	= Caffeine.from(REJECT_TIME_SETTING).build();

	/**
	 * F5攻撃カウント用キャッシュに値をセットします
	 * @param	String	キー
	 * @param	Integer	値
	 */
	public static void setAttackLimitCache(String key, Integer count) {
		attackLimitCache.put(key, count);
	}

	/**
	 * F5攻撃カウント用キャッシュから値を取得します
	 * @param	String				キー
	 * @return	Optional<Integer>	値
	 */
	public static Optional<Integer> getAttackLimitCache(String key) {
		return Optional.ofNullable(attackLimitCache.getIfPresent(key));
	}

	/**
	 * 遮断タイマー用キャッシュに値をセットします
	 * @param	String	キー
	 * @param	Integer	値
	 */
	public static void setRejectTimeCache(String key, Integer count) {
		rejectTimeCache.put(key, count);
	}

	/**
	 * 遮断タイマー用キャッシュから値を取得します
	 * @param	String				キー
	 * @return	Optional<Integer>	値
	 */
	public static Optional<Integer> getRejectTimeCache(String key) {
		return Optional.ofNullable(rejectTimeCache.getIfPresent(key));
	}

	/**
	 * リクエストからCookieを取得します
	 * @param request
	 * @param cookieName
	 * @return cookieOpt
	 */
	public static Optional<Cookie> getCookieValue(HttpServletRequest request, String cookieName) {
		// cookieNameの要素があれば取得（なければempty）
		return Arrays.stream(Optional.ofNullable(request.getCookies()).orElse(new Cookie[0])).filter(cookie -> cookie.getName().equals(cookieName)).findFirst();
	}

	/**
	 * CookieからセッションIDを取得します
	 * @param request
	 * @return sessionIdOpt
	 */
	public static Optional<String> getSessionId(ServletRequest request) {
		// Cookieがあれば返却（なければempty）
		return getCookieValue((HttpServletRequest) request, SESSION_ID_NAME).map(cookie -> cookie.getValue());
	}

	/**
	 * キャッシュの攻撃カウントをインクリメント／デクリメントして返します
	 * @param sessionId
	 * @param incCount
	 * @return attackCount
	 */
	synchronized public static Integer incAttackCount(String sessionId, Integer incCount) {
		// 値を取得してインクリメント
		final Integer attackCount = F5AttackUtil.getAttackLimitCache(sessionId).orElse(0) + incCount;
		if(attackCount >= 0) {
			// ゼロか正数の場合のみキャッシュにセット
			F5AttackUtil.setAttackLimitCache(sessionId, attackCount);
		}
		return attackCount;
	}
}
```

※間に合わせで作ったので内容は雑です。

# 4. テストコード

3種類のサーブレットクラスを用意します。

 * **Test1NoWait.java**：フィルタ対象ではない処理（レスポンス即時返却）
 * **Test2Wait1Sec.java**：1秒間のスリープを含む処理（フィルタ対象）
 * **Test3Wait4Sec.java**：4秒間のスリープを含む処理（フィルタ対象）

```java:Test1NoWait.java
package f5test;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class Test1NoWait
 */
@WebServlet("/Test1NoWait")
public class Test1NoWait extends HttpServlet {
	private static final long serialVersionUID = 1L;

    /**
     * @see HttpServlet#HttpServlet()
     */
    public Test1NoWait() {
        super();
        // TODO Auto-generated constructor stub
    }

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
        response.setContentType("text/html; charset=UTF-8");

        PrintWriter out = response.getWriter();
        out.println("test1_no_wait");
	}

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
		doGet(request, response);
	}

}
```

```java:Test2Wait1Sec.java
package f5test;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class Test2Wait1Sec
 */
@WebServlet("/Test2Wait1Sec")
public class Test2Wait1Sec extends HttpServlet {
	private static final long serialVersionUID = 1L;

    /**
     * @see HttpServlet#HttpServlet()
     */
    public Test2Wait1Sec() {
        super();
        // TODO Auto-generated constructor stub
    }

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
	    try{
	        Thread.sleep(1000);
	    } catch(InterruptedException e)
	    {
	    }

		response.setContentType("text/html; charset=UTF-8");

        PrintWriter out = response.getWriter();
        out.println("test2_wait_1_sec");
	}

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
		doGet(request, response);
	}

}
```

```java:Test3Wait4Sec.java
package f5test;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class Test3Wait4Sec
 */
@WebServlet("/Test3Wait4Sec")
public class Test3Wait4Sec extends HttpServlet {
	private static final long serialVersionUID = 1L;

    /**
     * @see HttpServlet#HttpServlet()
     */
    public Test3Wait4Sec() {
        super();
        // TODO Auto-generated constructor stub
    }

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
	    try{
	        Thread.sleep(4000);
	    } catch(InterruptedException e)
	    {
	    }

		response.setContentType("text/html; charset=UTF-8");

        PrintWriter out = response.getWriter();
        out.println("test3_wait_4_sec");
	}

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
		doGet(request, response);
	}

}
```

また、エラーページを作っておきます。

```text:error.html
Error!
```

※サーブレット・HTMLファイルとも、本来はHTMLを出力すべきですが、テストしやすくするためあえてテキストのみの出力としています。

以上を、Eclipse環境でTomcat 8.5を利用してサーバ起動しました。

一方で、テスト用のリクエストを投げるためのシェルスクリプトも作成します。

 * **request1.sh**：Test1NoWaitへのリクエスト（100連続→10秒休憩を繰り返す）
 * **request2.sh**：Test2Wait1Secへ連続10リクエスト送信し、その10秒後に1リクエスト、さらに40秒後に1リクエスト送信
 * **request3.sh**：Test3Wait4Secへ1秒間隔で20リクエスト送信し、その35秒後に1リクエスト送信
 * **request4.sh**：Test2Wait1Secへ1秒間隔で10リクエスト送信し、Test3Wait4Secへ4秒間隔で10リクエスト送信

```bash:request1.sh
#!/bin/sh
for FIG in `seq -f %04g 1 100`; do
  curl -s -b SID=00000000000000000000000000000001 http://localhost:8080/f5test/Test1NoWait > /home/h-mat/user1_${FIG}.txt &
done

sleep 8

for FIG in `seq -f %04g 1001 1100`; do
  curl -s -b SID=00000000000000000000000000000001 http://localhost:8080/f5test/Test1NoWait > /home/h-mat/user1_${FIG}.txt &
done

sleep 8

for FIG in `seq -f %04g 1101 1200`; do
  curl -s -b SID=00000000000000000000000000000001 http://localhost:8080/f5test/Test1NoWait > /home/h-mat/user1_${FIG}.txt &
done

sleep 8

for FIG in `seq -f %04g 1201 1300`; do
  curl -s -b SID=00000000000000000000000000000001 http://localhost:8080/f5test/Test1NoWait > /home/h-mat/user1_${FIG}.txt &
done
```

```bash:request2.sh
#!/bin/sh
for FIG in `seq -f %04g 1 10`; do
  curl -s -b SID=00000000000000000000000000000002 http://localhost:8080/f5test/Test2Wait1Sec > /home/h-mat/user2_${FIG}.txt &
done

sleep 10

curl -s -b SID=00000000000000000000000000000002 http://localhost:8080/f5test/Test2Wait1Sec > /home/h-mat/user2_1001.txt &

sleep 40

curl -s -b SID=00000000000000000000000000000002 http://localhost:8080/f5test/Test2Wait1Sec > /home/h-mat/user2_2001.txt &
```

```bash:request3.sh
#!/bin/sh
for FIG in `seq -f %04g 1 20`; do
  curl -s -b SID=00000000000000000000000000000003 http://localhost:8080/f5test/Test3Wait4Sec > /home/h-mat/user3_${FIG}.txt &
  sleep 1
done

sleep 35

curl -s -b SID=00000000000000000000000000000003 http://localhost:8080/f5test/Test3Wait4Sec > /home/h-mat/user3_1001.txt &
```

```bash:request4.sh
#!/bin/sh
for FIG in `seq -f %04g 1 10`; do
  curl -s -b SID=00000000000000000000000000000004 http://localhost:8080/f5test/Test2Wait1Sec > /home/h-mat/user4_${FIG}.txt &
  sleep 1
done

for FIG in `seq -f %04g 1001 1010`; do
  curl -s -b SID=00000000000000000000000000000004 http://localhost:8080/f5test/Test3Wait4Sec > /home/h-mat/user4_${FIG}.txt &
  sleep 4
done
```

※輪をかけて雑です。なお、ファイルの出力パスは適宜変更してください。

これらのうち、request2.shおよびrequest3.shの、途中から最後の1つ前までのレスポンスが「Error!」になれば正常に働いていることがわかります。

以上を、Cygwin上で実行してみました。

# 5. テスト結果

複数のCygwin Terminalを起動し、4つのスクリプトを同時並行で実行しました。

```bash:実行結果
h-mat@XXX ~
$ ls -l --full-time
合計 457
-rw-r--r-- 1 h-mat h-mat 708 2017-11-09 23:01:31.189777800 +0900 request1.sh
-rw-r--r-- 1 h-mat h-mat 441 2017-11-09 23:36:01.307228000 +0900 request2.sh
-rw-r--r-- 1 h-mat h-mat 318 2017-11-09 23:35:27.615581100 +0900 request3.sh
-rw-r--r-- 1 h-mat h-mat 367 2017-11-09 23:35:12.324463600 +0900 request4.sh
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:36:39.767421700 +0900 user1_0001.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:36:39.767421700 +0900 user1_0002.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:36:39.789569200 +0900 user1_0003.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:36:39.805200000 +0900 user1_0004.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:36:39.820826500 +0900 user1_0005.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:36:39.820826500 +0900 user1_0006.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:36:39.836453400 +0900 user1_0007.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:36:39.820826500 +0900 user1_0008.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:36:39.820826500 +0900 user1_0009.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:36:39.852079300 +0900 user1_0010.txt
（中略）
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:37:07.234792500 +0900 user1_1291.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:37:07.237800300 +0900 user1_1292.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:37:07.253341200 +0900 user1_1293.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:37:07.255346100 +0900 user1_1294.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:37:07.266376000 +0900 user1_1295.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:37:07.266877400 +0900 user1_1296.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:37:07.277906600 +0900 user1_1297.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:37:07.277906600 +0900 user1_1298.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:37:07.277906600 +0900 user1_1299.txt
-rw-r--r-- 1 h-mat h-mat  15 2017-11-09 23:37:07.288433900 +0900 user1_1300.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:42.401190900 +0900 user2_0001.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:42.401190900 +0900 user2_0002.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:42.421739600 +0900 user2_0003.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:41.442566900 +0900 user2_0004.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:41.473823900 +0900 user2_0005.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:41.458197000 +0900 user2_0006.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:41.458197000 +0900 user2_0007.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:41.458197000 +0900 user2_0008.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:41.458197000 +0900 user2_0009.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:41.473823900 +0900 user2_0010.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:51.521572800 +0900 user2_1001.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:37:32.540152100 +0900 user2_2001.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:47.031235300 +0900 user3_0001.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:48.075546900 +0900 user3_0002.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:49.115951500 +0900 user3_0003.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:46.155743600 +0900 user3_0004.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:47.189062300 +0900 user3_0005.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:48.227012700 +0900 user3_0006.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:49.571987400 +0900 user3_0007.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:50.331250400 +0900 user3_0008.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:51.390529100 +0900 user3_0009.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:52.437150400 +0900 user3_0010.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:53.484514200 +0900 user3_0011.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:54.508069700 +0900 user3_0012.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:55.570448600 +0900 user3_0013.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:56.629737900 +0900 user3_0014.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:57.704222600 +0900 user3_0015.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:58.746181500 +0900 user3_0016.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:36:59.783240400 +0900 user3_0017.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:37:00.823525500 +0900 user3_0018.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:37:01.831664900 +0900 user3_0019.txt
-rw-r--r-- 1 h-mat h-mat   6 2017-11-09 23:37:02.842442600 +0900 user3_0020.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:37:42.941408400 +0900 user3_1001.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:45.466315600 +0900 user4_0001.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:46.599671000 +0900 user4_0002.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:47.555378800 +0900 user4_0003.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:48.591186300 +0900 user4_0004.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:49.684096300 +0900 user4_0005.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:50.704652000 +0900 user4_0006.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:51.746813700 +0900 user4_0007.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:52.796479100 +0900 user4_0008.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:53.822635800 +0900 user4_0009.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:54.872509900 +0900 user4_0010.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:36:58.907968700 +0900 user4_1001.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:37:02.980534000 +0900 user4_1002.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:37:07.033757600 +0900 user4_1003.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:37:11.084447000 +0900 user4_1004.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:37:15.096440500 +0900 user4_1005.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:37:19.166343200 +0900 user4_1006.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:37:23.201475800 +0900 user4_1007.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:37:27.229357600 +0900 user4_1008.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:37:31.270541500 +0900 user4_1009.txt
-rw-r--r-- 1 h-mat h-mat  18 2017-11-09 23:37:35.296921400 +0900 user4_1010.txt

h-mat@XXX ~
$ fgrep 'Error!' user*.txt
user2_0004.txt:Error!
user2_0005.txt:Error!
user2_0006.txt:Error!
user2_0007.txt:Error!
user2_0008.txt:Error!
user2_0009.txt:Error!
user2_0010.txt:Error!
user2_1001.txt:Error!
user3_0004.txt:Error!
user3_0005.txt:Error!
user3_0006.txt:Error!
user3_0007.txt:Error!
user3_0008.txt:Error!
user3_0009.txt:Error!
user3_0010.txt:Error!
user3_0011.txt:Error!
user3_0012.txt:Error!
user3_0013.txt:Error!
user3_0014.txt:Error!
user3_0015.txt:Error!
user3_0016.txt:Error!
user3_0017.txt:Error!
user3_0018.txt:Error!
user3_0019.txt:Error!
user3_0020.txt:Error!
```

予定通り遮断されました。

# 6. 注意

テストでは、あえて連番の値をユーザ識別に使いましたが、このように、発行される値の法則性がわかりやすいものをユーザ識別に使うと、不正ユーザが正規ユーザに対してDoS攻撃を行う目的で悪用しやすくなってしまうため、JSESSIONIDのように推測が困難な値を使ってください。