`

手把手教你自定义IP访问次数限制器

阅读更多

前段时间弄爬虫的时候,在爬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秒。。。好吧。

大功告成!!!

 

原文就是我的,发表于我的个人博客

 

 

 

  • 大小: 1.4 MB
2
1
分享到:
评论
13 楼 843977358 2016-12-02  
xiaomayi1 写道
楼主QQ多少 想请教你几个问题

我昵称
12 楼 xiaomayi1 2016-08-29  
楼主QQ多少 想请教你几个问题
11 楼 843977358 2016-08-20  
hellotieye 写道
hellotieye 写道
你运行个代码 访问 iteye同一时间段 ,抓取能超1000次吗

说的明白点就 是 访问一个页面超过1000次 ,或不同页面200次。


超不过,
10 楼 843977358 2016-08-20  
somefuture 写道
843977358 写道
cs6641468 写道
楼主,你这代码写的太干了吧..... 还得多练.
另外,除了在多线程情况下用HashMap这种大坑,还有一些小建议:

1. filterLimitedIpMap(limitedIpMap)
问题1: 这种事情不应该是触发式的去做,而应该是定时做,最好是一个后台定时job去做这种事(最简单用一个TimerTask也可以),job的间隔用1分钟来控制误差. 这样不仅不频繁占用资源,效率也高很多,从代码来看,职责也被解耦。

问题2: 用entryMap代替keyMap


2. isLimitedIP(Map<String, Long> limitedIpMap, String ip)
为啥不直接用 limitedIpMap.contains方法


匆匆扫一眼,建议不准确的地方欢迎斧正


谢评!因为当时弄的时候参考了一点网上的东西,没考虑到线程安全方面的问题。我改了一版,用Hashtable代替了HashMap,另外,用定时器的话,虽然如您说能实现职责上的解耦,但是时间上也会造成一定的误差,并且只能尽量减少误差,并不能完全消除。再次感谢您的建议。

为何依然不选择使用ConcurrentHashMap


嗯嗯, 已经改过了,http://843977358.iteye.com/blog/2318143
9 楼 somefuture 2016-08-20  
843977358 写道
cs6641468 写道
楼主,你这代码写的太干了吧..... 还得多练.
另外,除了在多线程情况下用HashMap这种大坑,还有一些小建议:

1. filterLimitedIpMap(limitedIpMap)
问题1: 这种事情不应该是触发式的去做,而应该是定时做,最好是一个后台定时job去做这种事(最简单用一个TimerTask也可以),job的间隔用1分钟来控制误差. 这样不仅不频繁占用资源,效率也高很多,从代码来看,职责也被解耦。

问题2: 用entryMap代替keyMap


2. isLimitedIP(Map<String, Long> limitedIpMap, String ip)
为啥不直接用 limitedIpMap.contains方法


匆匆扫一眼,建议不准确的地方欢迎斧正


谢评!因为当时弄的时候参考了一点网上的东西,没考虑到线程安全方面的问题。我改了一版,用Hashtable代替了HashMap,另外,用定时器的话,虽然如您说能实现职责上的解耦,但是时间上也会造成一定的误差,并且只能尽量减少误差,并不能完全消除。再次感谢您的建议。

为何依然不选择使用ConcurrentHashMap
8 楼 hellotieye 2016-08-19  
hellotieye 写道
你运行个代码 访问 iteye同一时间段 ,抓取能超1000次吗

说的明白点就 是 访问一个页面超过1000次 ,或不同页面200次。
7 楼 hellotieye 2016-08-19  
你运行个代码 访问 iteye同一时间段 ,抓取能超1000次吗
6 楼 843977358 2016-08-18  
cs6641468 写道
用HashMap来存储, 我的天哪...

   
5 楼 843977358 2016-08-18  
knight_black_bob 写道
使用定时器什么的 感觉太过复杂,耗资源
建议使用 redis.expire(key, expiration)。。。。。

谢评!看了下你说的方法,可以解决。再次感谢您
4 楼 843977358 2016-08-18  
cs6641468 写道
楼主,你这代码写的太干了吧..... 还得多练.
另外,除了在多线程情况下用HashMap这种大坑,还有一些小建议:

1. filterLimitedIpMap(limitedIpMap)
问题1: 这种事情不应该是触发式的去做,而应该是定时做,最好是一个后台定时job去做这种事(最简单用一个TimerTask也可以),job的间隔用1分钟来控制误差. 这样不仅不频繁占用资源,效率也高很多,从代码来看,职责也被解耦。

问题2: 用entryMap代替keyMap


2. isLimitedIP(Map<String, Long> limitedIpMap, String ip)
为啥不直接用 limitedIpMap.contains方法


匆匆扫一眼,建议不准确的地方欢迎斧正


谢评!因为当时弄的时候参考了一点网上的东西,没考虑到线程安全方面的问题。我改了一版,用Hashtable代替了HashMap,另外,用定时器的话,虽然如您说能实现职责上的解耦,但是时间上也会造成一定的误差,并且只能尽量减少误差,并不能完全消除。再次感谢您的建议。
3 楼 knight_black_bob 2016-08-17  
使用定时器什么的 感觉太过复杂,耗资源
建议使用 redis.expire(key, expiration)。。。。。
2 楼 cs6641468 2016-08-17  
楼主,你这代码写的太干了吧..... 还得多练.
另外,除了在多线程情况下用HashMap这种大坑,还有一些小建议:

1. filterLimitedIpMap(limitedIpMap)
问题1: 这种事情不应该是触发式的去做,而应该是定时做,最好是一个后台定时job去做这种事(最简单用一个TimerTask也可以),job的间隔用1分钟来控制误差. 这样不仅不频繁占用资源,效率也高很多,从代码来看,职责也被解耦。

问题2: 用entryMap代替keyMap


2. isLimitedIP(Map<String, Long> limitedIpMap, String ip)
为啥不直接用 limitedIpMap.contains方法


匆匆扫一眼,建议不准确的地方欢迎斧正

1 楼 cs6641468 2016-08-17  
用HashMap来存储, 我的天哪...

相关推荐

    手把手教你学DSP:基于TMS320F28335

    手把手教你学DSP:基于TMS320F28335 手把手教你学DSP:基于TMS320F28335 手把手教你学DSP:基于TMS320F28335 手把手教你学DSP:基于TMS320F28335 手把手教你学DSP:基于TMS320F28335 手把手教你学DSP:基于TMS320F...

    Android 手把手教您自定义ViewGroup(一)

    我们将通过分析标题"Android 手把手教您自定义ViewGroup(一)"以及提供的博客链接,深入探讨这个主题。 首先,自定义ViewGroup意味着你需要扩展`android.view.ViewGroup`类,这是一个容器,可以包含多个子视图...

    手把手教你学28335

    手把手教你学28335PDF文档,看了这个确实和2812有了对比

    手把手教你学dsp2812,手把手教你学dsp2812pdf下载,C,C++

    《手把手教你学DSP2812》是一本专为初学者设计的 DSP(Digital Signal Processor)学习指南,主要围绕TI公司的TMS320F2812 DSP芯片进行讲解。这本书以其全面且易懂的特性,为读者提供了一个深入理解数字信号处理及其...

    手把手教你学DSP28335

    手把手教你学DSP28335,PDF格式,有助于随时随地可以学习知识。

    手把手教你学DSPPDF

    【标题】"手把手教你学DSPPDF"是一份针对数字信号处理(DSP)初学者的教程性PDF文档,旨在引领读者逐步掌握这一领域的基础知识。该文档可能包含了从理论概念到实际应用的全面讲解,适合那些希望踏入数字信号处理世界...

    手把手教你dsp28335,高清pdf

    手把手教你学DSP28335高清pdf文件,北京航空航天大学出版社

    手把手教你学dsp

    手把手教你学dsp F2812 顾伟刚

    《手把手教你学51单片机》教材pdf

    《手把手教你学51单片机》是一本专为初学者设计的嵌入式开发入门教程,旨在帮助读者从零开始掌握51单片机的基础知识和应用技能。51单片机是嵌入式系统中最基础且广泛应用的一类微控制器,广泛应用于智能家居、工业...

    003《老HRD手把手教你做绩效考核》.pdf

    003《老HRD手把手教你做绩效考核》.pdf

    手把手教你DSP配套资料

    “手把手教你DSP配套资料”这一压缩包很可能是包含了一系列关于DSP的学习材料,可能包括教程文档、示例代码、实验指导等。通过这些资料,你可以深入理解DSP的基本原理、算法和应用,逐步掌握实际操作技能,为你的...

    手把手教你学单片机(第二版) 周兴华.pdf

    手把手教你学单片机(第二版) 周兴华.pdf

    手把手教你学51单片机-资源

    《手把手教你学51单片机》是宋雪松先生编写的一本深入浅出的单片机学习教程,特别适合初学者入门。51单片机是微控制器领域非常经典的一款芯片,由Intel公司推出,因其8051内核而得名,现在由许多厂商如Atmel、...

    《手把手教你学dsp2812》顾卫刚.PDF

    根据给出的文件信息,《手把手教你学dsp2812》是顾卫刚所著的一本关于DSP2812的教材书籍。DSP2812指的是德州仪器(Texas Instruments,简称TI)的TMS320F2812数字信号处理器。TMS320F2812是一款32位的高性能数字信号...

    手把手教你学51单片机C语言版PDF

    《手把手教你学51单片机C语言版》是一本专为初学者设计的教程,旨在帮助读者从零开始掌握51系列单片机的编程与应用。这本书由权威的电子技术教育平台www.kingst.org提供,是学习单片机C语言编程的宝贵资源。下面将...

    手把手教你如何从一无所有到财务自由.pdf

    "手把手教你如何从一无所有到财务自由.pdf" 本文主要讲述的是如何从零开始创业,到达财务自由的三个步骤。作者通过幽默诙谐的语言,浅显易懂的思维高度,指导小屁孩创业,并提供了非常好的指引方向。 第一部分:...

    《Qt5 PyQt 5实战指南-手把手教你掌握100个精彩案例》.rar

    《Qt5 PyQt 5实战指南—手把手教你掌握100个精彩案例》 《Qt5 PyQt 5实战指南—手把手教你掌握100个精彩案例》 《Qt5 PyQt 5实战指南—手把手教你掌握100个精彩案例》

    手把手教你打造apk安装器

    【标题】"手把手教你打造apk安装器"的教程涵盖了创建和使用自定义APK安装程序的核心技术。在Android平台上,APK是应用程序的二进制格式,通常通过Google Play或其他应用商店进行分发。然而,有时候我们可能需要创建...

    手把手教你单片机程序框架

    手把手教你单片机程序框架 手把手教你单片机程序框架 手把手教你单片机程序框架

Global site tag (gtag.js) - Google Analytics