前段時間弄爬蟲的時候,在爬iteye的時候碰到過一個場景,Spider跑幾次或者抓取的時間間隔小一點之後就會出現401錯誤
16-08-16 15:05:49,687 INFO us.codecraft.webmagic.Spider(Spider.java:307) ## Spider 843977358.iteye.com started!
16-08-16 15:05:49,696 INFO us.codecraft.webmagic.downloader.HttpClientDownloader(HttpClientDownloader.java:87) ## downloading page http://843977358.iteye.com/
16-08-16 15:05:50,056 WARN us.codecraft.webmagic.downloader.HttpClientDownloader(HttpClientDownloader.java:100) ## code error 401 http://843977358.iteye.com/
然後,此時在去訪問iteye就會提示你“您所在的IP地址對ITeye網站訪問過於頻繁,爲了判斷您的訪問是真實用戶,請您填寫驗證碼,謝謝!”
不用想,一定是人家後臺給限制住了。
因此,自己就瞎鼓搗搞了一個簡單點的,僅僅可以實現對用戶IP次數的檢查和對違規用戶的封禁。
用到的技術:
過濾器(Filter):統計用戶訪問次數,記錄訪問時間、封禁時間
監聽器(Listener):工程運行時初始化IP存儲器(此處用的Map)
我的思路:
工程啓動時,創建兩個Map,一個(ipMap)用來存放用戶Ip和訪問時間等主要信息,另一個(limitedIpMap)用來存放被限制的用戶IP。Map的key爲用戶的IP,value爲具體內容。
當用戶訪問系統時,通過IPFilter檢查limitedIpMap中是否存在當前IP,如果存在說明該IP之前存在過惡意刷新訪問,已經被限制,跳轉到異常提示頁面;如果limitedIpMap
中不存在則檢查ipMap中是否存在當前IP,如果ipMap中不存在則說明用戶初次訪問,用戶訪問次數+1,初始訪問時間爲當前時間;如果存在則檢查用戶訪問次數是否在規定的短時間內進行了大量的訪問操作;如果是,則將當前IP添加到limitedIpMap中,並跳轉到異常提示頁面,否則不進行操作,直接放行本次請求。
(簡單畫了下流程圖,看不懂的留言問我話)
配置文件:
<!-- 配置過濾器 start --> <filter> <filter-name>IPFilter</filter-name> <filter-class>com.test.interceptor.IPFilter</filter-class> </filter> <filter-mapping> <filter-name>IPFilter</filter-name> <url-pattern>/render/*</url-pattern> </filter-mapping> <!-- 配置過濾器 end --> <!-- 配置監聽器 start --> <listener> <listener-class>com.test.listener.MyListener</listener-class> </listener> <!-- 配置監聽器 start -->
監聽器MyListener:
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
/**
* @Description 自定義監聽器,項目啓動時初始化兩個全局的map,
* ipMap(ip存儲器,記錄IP的訪問次數、訪問時間)
* limitedIpMap(限制IP存儲器)用來存儲每個訪問用戶的IP以及訪問的次數
* @author zhangyd
* @date 2016年7月28日 下午5:47:23
* @since JDK : 1.7
*/
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
// IP存儲器
Map<String, Long[]> ipMap = new HashMap<String, Long[]>();
context.setAttribute("ipMap", ipMap);
// 限制IP存儲器:存儲被限制的IP信息
Map<String, Long> limitedIpMap = new HashMap<String, Long>();
context.setAttribute("limitedIpMap", limitedIpMap);
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
過濾器IPFilter:
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.test.util.IPUtil;
/**
*
* @Description 自定義過濾器,用來判斷IP訪問次數是否超限。<br>
* 如果前臺用戶訪問網站的頻率過快(比如:達到或超過50次/s),則判定該IP爲惡意刷新操作,限制該ip訪問<br>
* 默認限制訪問時間爲1小時,一小時後自定解除限制
*
* @author zhangyd
* @date 2016年7月28日 下午5:54:51
* @since JDK : 1.7
*/
public class IPFilter implements Filter {
/**
* 默認限制時間(單位:ms)
*/
private static final long LIMITED_TIME_MILLIS = 60 * 60 * 1000;
/**
* 用戶連續訪問最高閥值,超過該值則認定爲惡意操作的IP,進行限制
*/
private static final int LIMIT_NUMBER = 20;
/**
* 用戶訪問最小安全時間,在該時間內如果訪問次數大於閥值,則記錄爲惡意IP,否則視爲正常訪問
*/
private static final int MIN_SAFE_TIME = 5000;
private FilterConfig config;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.config = filterConfig;
}
/**
* @Description 核心處理代碼
* @param servletRequest
* @param servletResponse
* @param chain
* @throws IOException
* @throws ServletException
* @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
* javax.servlet.ServletResponse, javax.servlet.FilterChain)
*/
@SuppressWarnings("unchecked")
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
ServletContext context = config.getServletContext();
// 獲取限制IP存儲器:存儲被限制的IP信息
Map<String, Long> limitedIpMap = (Map<String, Long>) context.getAttribute("limitedIpMap");
// 過濾受限的IP
filterLimitedIpMap(limitedIpMap);
// 獲取用戶IP
String ip = IPUtil.getIp(request);
// 判斷是否是被限制的IP,如果是則跳到異常頁面
if (isLimitedIP(limitedIpMap, ip)) {
long limitedTime = limitedIpMap.get(ip) - System.currentTimeMillis();
// 剩餘限制時間(用爲從毫秒到秒轉化的一定會存在些許誤差,但基本可以忽略不計)
request.setAttribute("remainingTime", ((limitedTime / 1000) + (limitedTime % 1000 > 0 ? 1 : 0)));
request.getRequestDispatcher("/error/overLimitIP").forward(request, response);
return;
}
// 獲取IP存儲器
Map<String, Long[]> ipMap = (Map<String, Long[]>) context.getAttribute("ipMap");
// 判斷存儲器中是否存在當前IP,如果沒有則爲初次訪問,初始化該ip
// 如果存在當前ip,則驗證當前ip的訪問次數
// 如果大於限制閥值,判斷達到閥值的時間,如果不大於[用戶訪問最小安全時間]則視爲惡意訪問,跳轉到異常頁面
if (ipMap.containsKey(ip)) {
Long[] ipInfo = ipMap.get(ip);
ipInfo[0] = ipInfo[0] + 1;
System.out.println("當前第[" + (ipInfo[0]) + "]次訪問");
if (ipInfo[0] > LIMIT_NUMBER) {
Long ipAccessTime = ipInfo[1];
Long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis - ipAccessTime <= MIN_SAFE_TIME) {
limitedIpMap.put(ip, currentTimeMillis + LIMITED_TIME_MILLIS);
request.setAttribute("remainingTime", LIMITED_TIME_MILLIS);
request.getRequestDispatcher("/error/overLimitIP").forward(request, response);
return;
} else {
initIpVisitsNumber(ipMap, ip);
}
}
} else {
initIpVisitsNumber(ipMap, ip);
System.out.println("您首次訪問該網站");
}
context.setAttribute("ipMap", ipMap);
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
/**
* @Description 是否是被限制的IP
* @author zhangyd
* @date 2016年8月8日 下午5:39:17
* @param limitedIpMap
* @param ip
* @return true : 被限制 | false : 正常
*/
private boolean isLimitedIP(Map<String, Long> limitedIpMap, String ip) {
if (limitedIpMap == null || ip == null) {
// 沒有被限制
return false;
}
Set<String> keys = limitedIpMap.keySet();
Iterator<String> keyIt = keys.iterator();
while (keyIt.hasNext()) {
String key = keyIt.next();
if (key.equals(ip)) {
// 被限制的IP
return true;
}
}
return false;
}
/**
* @Description 過濾受限的IP,剔除已經到期的限制IP
* @author zhangyd
* @date 2016年8月8日 下午5:34:33
* @param limitedIpMap
*/
private void filterLimitedIpMap(Map<String, Long> limitedIpMap) {
if (limitedIpMap == null) {
return;
}
Set<String> keys = limitedIpMap.keySet();
Iterator<String> keyIt = keys.iterator();
long currentTimeMillis = System.currentTimeMillis();
while (keyIt.hasNext()) {
long expireTimeMillis = limitedIpMap.get(keyIt.next());
if (expireTimeMillis <= currentTimeMillis) {
keyIt.remove();
}
}
}
/**
* 初始化用戶訪問次數和訪問時間
*
* @author zhangyd
* @date 2016年7月29日 上午10:01:39
* @param ipMap
* @param ip
*/
private void initIpVisitsNumber(Map<String, Long[]> ipMap, String ip) {
Long[] ipInfo = new Long[2];
ipInfo[0] = 0L;// 訪問次數
ipInfo[1] = System.currentTimeMillis();// 初次訪問時間
ipMap.put(ip, ipInfo);
}
}
爲了方便測試,我把封禁時間調到1分鐘
/**
* 默認限制時間(單位:ms)
*/
private static final long LIMITED_TIME_MILLIS = 60 * 1000;
/**
* 用戶連續訪問最高閥值,超過該值則認定爲惡意操作的IP,進行限制
*/
private static final int LIMIT_NUMBER = 20;
/**
* 用戶訪問最小安全時間,在該時間內如果訪問次數大於閥值,則記錄爲惡意IP,否則視爲正常訪問
*/
private static final int MIN_SAFE_TIME = 5000;
上面這三項是自定義的,根據自己情況來。
測試:
演示統共分三步:
第一步:正常訪問並且間隔時間略長,訪問20次爲第一步
第二步:按住F5狂刷,一直到跳轉到限制頁面爲第二步
第三步:等待1min,限制時間過後,重新刷新頁面
(此處沒有大象......)
我有罪,我不該把時間調成1min的, 應該20秒。。。好吧。
大功告成!!!