方法一、自定义注解
利用spring切面或拦截器实现;
可以对所有接口或添加注解的接口实现请求次数限制;
利用缓存的过期时间功能,uri太长可以做个映射(为缓存节省空间);
ip要防止利用代理访问;
限制维度:ip级别、用户+接口级别;
*******************************注解类
[java] view plain copy
1. /**
2. * request请求次数限制
3. * 示例:@RequestLimit(ipCount=2,ipTime=60000,uriCount=2,uriTime=60000)
4. * @date 2016-03-23 13:28:06
5. */
6. @Target(ElementType.METHOD)
7. @Retention(RetentionPolicy.RUNTIME)
8. @Inherited
9. @Documented
10. public @interface RequestLimit {
11.
12. /**
13. * ip允许访问的次数,默认值1000
14. */
15. int ipCount() default 1000;
16.
17. /**
18. * ip时间段,单位为毫秒,默认值一分钟
19. */
20. long ipTime() default 60000;
21.
22. /**
23. * uri允许访问的次数,默认值600
24. */
25. int uriCount() default 600;
26.
27. /**
28. * uri时间段,单位为毫秒,默认值一分钟
29. */
30. long uriTime() default 60000;
31.
32. }
*******************************
*******************************spring切面
[java] view plain copy
1. public Object around(ProceedingJoinPoint pjp) {
2. //拦截的实体类
3. Object target = pjp.getTarget();
4. //拦截的方法名称
5. String methodName = pjp.getSignature().getName();
6. //拦截的放参数类型
7. Class[] parameterTypes = ((MethodSignature)pjp.getSignature()).getMethod().getParameterTypes();
8. Class[] clazzs = target.getClass().getInterfaces();
9. //1.获取类
10. Class clazz = target.getClass();
11. if (clazzs != null && clazzs.length > 0){
12. clazz = clazzs[0];
13. }
14. //2.获取方法
15. Method m = clazz.getMethod(methodName, parameterTypes);
16. //3.获取request、callback
17. Object[] args = pjp.getArgs();
18. HttpServletRequest request = null;
19. if (args != null && args.length > 0) {
20. if (args[0] instanceof HttpServletRequest) {
21. request = (HttpServletRequest) args[0];
22. if(request != null){
23. callback = request.getParameter("callback");
24. }
25. }
26. }
27. //RequestLimit判断
28. String reequestLimitRes = this.RequestLimitCheck(m, request);
29. if("fail".equals(reequestLimitRes)){
30. return "fail";//返回值改为自己的格式
31. }else{
32. Object obj = pjp.proceed();
33. }
34. }
35.
36. private String RequestLimitCheck(Method m, HttpServletRequest request) throws IOException{
37. //ip、user_phone+uri 两个维度的访问限制
38. if(m!=null && m.isAnnotationPresent(RequestLimit.class)){
39. RequestLimit requestLimit = m.getAnnotation(RequestLimit.class);
40. //失效时间、访问次数
41. int ipTime = (int) (requestLimit.ipTime() / 1000);
42. int ipCount = requestLimit.ipCount();
43. int uriTime = (int) (requestLimit.uriTime() / 1000);
44. int uriCount = requestLimit.uriCount();
45.
46. //ip、user_phone、uri
47. String ip_key = NetworkUtil.getIpAddress(request);//ip:获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址;
48. String user_phone = request.getParameter("user_phone");//手机号
49. String uri = request.getRequestURI().toString();//uri:如果字符串太长,可以做个映射
50. String user_uri_key = user_phone + uri;
51. //先从缓存中(以Memcached为例)获取两个key对应的value值,value值为访问限制次数
52. List<String> keyCollections = new ArrayList<String>();
53. keyCollections.add(ip_key);
54. keyCollections.add(user_uri_key);
55. Map<String, String> valueMap = memcachedClient.get(keyCollections);//一次读取出多个key_value
56. Integer ipNumCache = 0;//ip访问次数
57. Integer userUriNumCache = 0;//手机号+uri访问次数
58. if(valueMap!=null && valueMap.size()>0){
59. String ipNumCacheFlag = valueMap.get(ip_key);
60. if(StringUtils.isNotBlank(ipNumCacheFlag)){
61. ipNumCache = Integer.parseInt(ipNumCacheFlag);
62. }
63. String userUriNumCacheFlag = valueMap.get(user_uri_key);
64. if(StringUtils.isNotBlank(userUriNumCacheFlag)){
65. userUriNumCache = Integer.parseInt(userUriNumCacheFlag);
66. }
67. }
68. //ip限制判断
69. if(ipNumCache == 0){
70. memcachedClient.set(ip_key, 1, ipTime);
71. }else if(ipNumCache >= ipCount){
72. logger.info("request_limit:用户IP[" + ip_key + "],超过了限定的次数[" + ipCount + "]");
73. return "fail";
74. }else{
75. memcachedClient.incr(ip_key, 1);//自增
76. }
77. //user_phone、uri限制判断
78. if(userUriNumCache == 0){
79. memcachedClient.set(user_uri_key, 1, uriTime);
80. }else if(userUriNumCache >= uriCount){
81. logger.info("request_limit:用户手机号[" + user_phone + "],访问地址[" + uri + "],超过了限定的次数[" + uriCount + "]");
82. return "fail";
83. }else{
84. memcachedClient.incr(user_uri_key, 1);
85. }
86. }
87. return "success";
88. }
*******************************
后续:
可以将不正常调用的用户添加进黑名单,保存在数据库中...
爬虫项目会定期定时访问接口,后台分析...
方法二、使用缓存实现
数据访问量大的话 用redis来做,用户在调用短信接口时,先根据用户id去查一下次数,如果没有id这个key,证明就是1分钟内首次发送,发送后,在redis中记录一个key为id的次数为一次,在为这个key加上过期时间1分钟
方法三、过滤器、监听器
工程启动时,创建两个Map,一个(ipMap)用来存放用户Ip和访问时间等主要信息,另一个(limitedIpMap)用来存放被限制的用户IP。Map的key为用户的IP,value为具体内容。
当用户访问系统时,通过IPFilter检查limitedIpMap中是否存在当前IP,如果存在说明该IP之前存在过恶意刷新访问,已经被限制,跳转到异常提示页面;如果limitedIpMap
中不存在则检查ipMap中是否存在当前IP,如果ipMap中不存在则说明用户初次访问,用户访问次数+1,初始访问时间为当前时间;如果存在则检查用户访问次数是否在规定的短时间内进行了大量的访问操作;如果是,则将当前IP添加到limitedIpMap中,并跳转到异常提示页面,否则不进行操作,直接放行本次请求。
配置文件:
1. <!-- 配置过滤器 start -->
2. <filter>
3. <filter-name>IPFilter</filter-name>
4. <filter-class>com.test.interceptor.IPFilter</filter-class>
5. </filter>
6. <filter-mapping>
7. <filter-name>IPFilter</filter-name>
8. <url-pattern>/render/*</url-pattern>
9. </filter-mapping>
10.<!-- 配置过滤器 end -->
11.
12.<!-- 配置监听器 start -->
13.<listener>
14. <listener-class>com.test.listener.MyListener</listener-class>
15.</listener>
16.<!-- 配置监听器 start -->
监听器MyListener:
1. import java.util.concurrent.ConcurrentHashMap;
2.
3. import javax.servlet.ServletContext;
4. import javax.servlet.ServletContextEvent;
5. import javax.servlet.ServletContextListener;
6.
7. /**
8. * @Description 自定义监听器,项目启动时初始化两个全局的ConcurrentHashMap(线程安全)
9. * ipMap(ip存储器,记录IP的访问次数、访问时间)
10. * limitedIpMap(限制IP存储器)用来存储每个访问用户的IP以及访问的次数
11. * @author zhangyd
12. * @date 2016年7月28日 下午5:47:23
13. * @since JDK : 1.7
14. * @version 2.0
15. * @modify 改hashMap 为 ConcurrentHashMap
16. */
17. public class MyListener implements ServletContextListener {
18.
19. @Override
20. public void contextInitialized(ServletContextEvent sce) {
21. ServletContext context = sce.getServletContext();
22. // IP存储器
23. ConcurrentHashMap<String, Long[]> ipMap = new ConcurrentHashMap<String, Long[]>();
24. context.setAttribute("ipMap", ipMap);
25. // 限制IP存储器:存储被限制的IP信息
26. ConcurrentHashMap<String, Long> limitedIpMap = new ConcurrentHashMap<String, Long>();
27. context.setAttribute("limitedIpMap", limitedIpMap);
28. }
29.
30. @Override
31. public void contextDestroyed(ServletContextEvent sce) {
32.
33. }
34.
35. }
过滤器IPFilter:
1. import java.io.IOException;
2. import java.util.concurrent.ConcurrentHashMap;
3.
4. import javax.servlet.Filter;
5. import javax.servlet.FilterChain;
6. import javax.servlet.FilterConfig;
7. import javax.servlet.ServletContext;
8. import javax.servlet.ServletException;
9. import javax.servlet.ServletRequest;
10. import javax.servlet.ServletResponse;
11. import javax.servlet.http.HttpServletRequest;
12. import javax.servlet.http.HttpServletResponse;
13.
14. import com.test.util.IPUtil;
15.
16. /**
17. *
18. * @Description 自定义过滤器,用来判断IP访问次数是否超限。<br>
19. * 如果前台用户访问网站的频率过快(比如:达到或超过50次/s),则判定该IP为恶意刷新操作,限制该ip访问<br>
20. * 默认限制访问时间为1小时,一小时后自定解除限制
21. *
22. * @author zhangyd
23. * @date 2016年7月28日 下午5:54:51
24. * @since JDK : 1.7
25. * @version 2.0
26. * @modify 改hashMap 为 线程安全的ConcurrentHashMap
27. */
28. public class IPFilter implements Filter {
29.
30. /** 默认限制时间(单位:ms) */
31. private static final long LIMITED_TIME_MILLIS = 60 * 1000;
32.
33. /** 用户连续访问最高阀值,超过该值则认定为恶意操作的IP,进行限制 */
34. private static final int LIMIT_NUMBER = 20;
35.
36. /** 用户访问最小安全时间,在该时间内如果访问次数大于阀值,则记录为恶意IP,否则视为正常访问 */
37. private static final int MIN_SAFE_TIME = 5000;
38.
39. private FilterConfig config;
40.
41. @Override
42. public void init(FilterConfig filterConfig) throws ServletException {
43. this.config = filterConfig;
44. }
45.
46. /**
47. * @Description 核心处理代码
48. * @param servletRequest
49. * @param servletResponse
50. * @param chain
51. * @throws IOException
52. * @throws ServletException
53. * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
54. * javax.servlet.ServletResponse, javax.servlet.FilterChain)
55. */
56. @SuppressWarnings("unchecked")
57. @Override
58. public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
59. throws IOException, ServletException {
60. HttpServletRequest request = (HttpServletRequest) servletRequest;
61. HttpServletResponse response = (HttpServletResponse) servletResponse;
62.
63. ServletContext context = config.getServletContext();
64. // 获取限制IP存储器:存储被限制的IP信息
65. ConcurrentHashMap<String, Long> limitedIpMap = (ConcurrentHashMap<String, Long>) context
66. .getAttribute("limitedIpMap");
67. // 获取用户IP
68. String ip = IPUtil.getIp(request);
69. // 判断是否是被限制的IP,如果是则跳到异常页面
70. if (isLimitedIP(limitedIpMap, ip)) {
71. long limitedTime = limitedIpMap.get(ip) - System.currentTimeMillis();
72. forward(request, response, ((limitedTime / 1000) + (limitedTime % 1000 > 0 ? 1 : 0)));
73. return;
74. }
75. // 获取IP存储器
76. ConcurrentHashMap<String, Long[]> ipMap = (ConcurrentHashMap<String, Long[]>) context.getAttribute("ipMap");
77.
78. // 判断存储器中是否存在当前IP,如果没有则为初次访问,初始化该ip
79. // 如果存在当前ip,则验证当前ip的访问次数
80. // 如果大于限制阀值,判断达到阀值的时间,如果不大于[用户访问最小安全时间]则视为恶意访问,跳转到异常页面
81. if (ipMap.containsKey(ip)) {
82. Long[] ipInfo = ipMap.get(ip);
83. ipInfo[0] = ipInfo[0] + 1;
84. if (ipInfo[0] > LIMIT_NUMBER) {
85. Long ipAccessTime = ipInfo[1];
86. Long currentTimeMillis = System.currentTimeMillis();
87. // 限制时间内
88. if (currentTimeMillis - ipAccessTime <= MIN_SAFE_TIME) {
89. System.out
90. .println(ip + " 在[" + (currentTimeMillis - ipAccessTime) + "]ms内,共访问了[" + ipInfo[0] + "]次");
91. limitedIpMap.put(ip, currentTimeMillis + LIMITED_TIME_MILLIS);
92. forward(request, response, currentTimeMillis + LIMITED_TIME_MILLIS);
93. return;
94. } else {
95. initIpVisitsNumber(ipMap, ip);
96. }
97. }
98. } else {
99. initIpVisitsNumber(ipMap, ip);
100. }
101. context.setAttribute("ipMap", ipMap);
102. chain.doFilter(request, response);
103. }
104.
105. @Override
106. public void destroy() {
107.
108. }
109.
110. /**
111. * @Description 跳转页面
112. * @author zhangyd
113. * @date 2016年8月17日 下午5:58:43
114. * @param request
115. * @param response
116. * @param remainingTime
117. * 剩余限制时间
118. * @throws ServletException
119. * @throws IOException
120. */
121. private void forward(HttpServletRequest request, HttpServletResponse response, long remainingTime)
122. throws ServletException, IOException {
123. request.setAttribute("remainingTime", remainingTime);
124. request.getRequestDispatcher("/error/overLimitIP").forward(request, response);
125. }
126.
127. /**
128. * @Description 是否是被限制的IP
129. * @author zhangyd
130. * @date 2016年8月8日 下午5:39:17
131. * @param limitedIpMap
132. * @param ip
133. * @return true : 被限制 | false : 正常
134. */
135. private boolean isLimitedIP(ConcurrentHashMap<String, Long> limitedIpMap, String ip) {
136. if (limitedIpMap == null || limitedIpMap.isEmpty() || ip == null) {
137. // 没有被限制
138. return false;
139. }
140. return limitedIpMap.containsKey(ip);
141. }
142.
143. /**
144. * 初始化用户访问次数和访问时间
145. *
146. * @author zhangyd
147. * @date 2016年7月29日 上午10:01:39
148. * @param ipMap
149. * @param ip
150. */
151. private void initIpVisitsNumber(ConcurrentHashMap<String, Long[]> ipMap, String ip) {
152. Long[] ipInfo = new Long[2];
153. ipInfo[0] = 0L;// 访问次数
154. ipInfo[1] = System.currentTimeMillis();// 初次访问时间
155. ipMap.put(ip, ipInfo);
156. }
157.
158. }
为了方便测试,我把封禁时间调到1分钟
1. /**
2. * 默认限制时间(单位:ms)
3. */
4. private static final long LIMITED_TIME_MILLIS = 60 * 1000;
5.
6. /**
7. * 用户连续访问最高阀值,超过该值则认定为恶意操作的IP,进行限制
8. */
9. private static final int LIMIT_NUMBER = 20;
10.
11./**
12. * 用户访问最小安全时间,在该时间内如果访问次数大于阀值,则记录为恶意IP,否则视为正常访问
13. */
14.private static final int MIN_SAFE_TIME = 5000;
上面这三项是自定义的,根据自己情况来。
加入Spring Task定时器。配置文件:applicationContext.xml
1. <context:component-scan base-package="com.test" >
2. <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />
3. </context:component-scan>
4. <!-- 配置定时任务 -->
5. <task:annotation-driven />
IPFilterTask
1. import java.util.Date;
2. import java.util.Iterator;
3. import java.util.Map.Entry;
4. import java.util.concurrent.ConcurrentHashMap;
5.
6. import javax.servlet.ServletContext;
7.
8. import org.springframework.scheduling.annotation.Scheduled;
9. import org.springframework.stereotype.Component;
10. import org.springframework.web.context.ContextLoaderListener;
11.
12. /**
13. * @Description IP限制过滤器定时任务,过滤受限的IP,剔除已经到期的限制IP
14. * @author zhangyd
15. * @date 2016年8月18日 上午9:39:19
16. * @version V1.0
17. * @since JDK : 1.7
18. * @modify 将IP限制过滤手动触发改为定时任务
19. */
20. @Component
21. public class IPFilterTask {
22.
23. /**
24. * @Description 30s执行一次过滤操作
25. * @author zhangyd
26. * @date 2016年8月17日 下午5:49:55
27. */
28. @Scheduled(cron = "0/30 * * * * ? ")
29. public void filterLimitedIpMap() {
30. ServletContext context = ContextLoaderListener.getCurrentWebApplicationContext().getServletContext();
31. @SuppressWarnings("unchecked")
32. ConcurrentHashMap<String, Long> limitedIpMap = (ConcurrentHashMap<String, Long>) context
33. .getAttribute("limitedIpMap");
34. if (limitedIpMap.isEmpty()) {
35. return;
36. }
37. Iterator<Entry<String, Long>> it = limitedIpMap.entrySet().iterator();
38. long currentTimeMillis = System.currentTimeMillis();
39. while (it.hasNext()) {
40. Entry<String, Long> e = it.next();
41. long expireTimeMillis = e.getValue();
42. if (expireTimeMillis <= currentTimeMillis) {
43. it.remove();
44. System.out.println(new Date() + "时,去掉了一个限制用户[" + e.getKey() + "]");
45. }
46. }
47. }
48. }
测试:
演示统共分三步:
第一步:正常访问并且间隔时间略长,访问20次为第一步
第二步:按住F5狂刷,一直到跳转到限制页面为第二步
第三步:等待1min,限制时间过后,重新刷新页面
方法四、session控制
public void doGet(HttpServletRequestrequest, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out
.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTDHTML 4.01 Transitional//EN\">");
out.println("<HTML>");
out.println(" <HEAD><TITLE>AServlet</TITLE></HEAD>");
out.println(" <BODY>");
HttpSession session = request.getSession(true);
Object count = session.getAttribute("COUNTER");
int counter = 0;
if (count == null) {
counter = 1;
// 将第一次计数存入session
session.setAttribute("COUNTER", new Integer(1));
} else {
counter = ((Integer) count).intValue();
counter++;// 计数加一
// 将计数存入session
session.setAttribute("COUNTER", newInteger(counter));
}
// 输出信息
out.println(" 欢迎你" + counter + "次访问xx网站!");
out.println(" </BODY>");
out.println("</HTML>");
out.flush();
out.close();
}