手记

【修订版】手把手教你自定义IP访问次数限制器


小象就是我,踢死我吧。。。
第一版(点击这儿)写的有点匆匆忙忙,也没有仔细考虑,忽略了线程安全方面的问题,HashMap为线程非安全的,在多线程情况下用HashMap一定会出问题。因此,这一版将HashMap改为了比较安全的ConcurrentHashMap另外可以了解下(聊聊并发(四)深入分析ConcurrentHashMap)。filterLimitedIpMap方法,修改为了Task定时进行操作(虽然存在误差,但误差很小的话,基本可以忽略不)
附第二版部分代码:

监听器MyListener:

import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

/**
 * @Description 自定义监听器,项目启动时初始化两个全局的ConcurrentHashMap(线程安全)
 *              ipMap(ip存储器,记录IP的访问次数、访问时间)
 *              limitedIpMap(限制IP存储器)用来存储每个访问用户的IP以及访问的次数
 * @author zhangyd
 * @date 2016年7月28日 下午5:47:23
 * @since JDK : 1.7
 * @version 2.0
 * @modify 改hashMap 为 ConcurrentHashMap
 */
public class MyListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext context = sce.getServletContext();
        // IP存储器
        ConcurrentHashMap<String, Long[]> ipMap = new ConcurrentHashMap<String, Long[]>();
        context.setAttribute("ipMap", ipMap);
        // 限制IP存储器:存储被限制的IP信息
        ConcurrentHashMap<String, Long> limitedIpMap = new ConcurrentHashMap<String, Long>();
        context.setAttribute("limitedIpMap", limitedIpMap);
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {

    }

}

过滤器IPFilter

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

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
 * @version 2.0
 * @modify 改hashMap 为 线程安全的ConcurrentHashMap
 */
public class IPFilter implements Filter {

    /** 默认限制时间(单位: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;

    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信息
        ConcurrentHashMap<String, Long> limitedIpMap = (ConcurrentHashMap<String, Long>) context
                .getAttribute("limitedIpMap");
        // 获取用户IP
        String ip = IPUtil.getIp(request);
        // 判断是否是被限制的IP,如果是则跳到异常页面
        if (isLimitedIP(limitedIpMap, ip)) {
            long limitedTime = limitedIpMap.get(ip) - System.currentTimeMillis();
            forward(request, response, ((limitedTime / 1000) + (limitedTime % 1000 > 0 ? 1 : 0)));
            return;
        }
        // 获取IP存储器
        ConcurrentHashMap<String, Long[]> ipMap = (ConcurrentHashMap<String, Long[]>) context.getAttribute("ipMap");

        // 判断存储器中是否存在当前IP,如果没有则为初次访问,初始化该ip
        // 如果存在当前ip,则验证当前ip的访问次数
        // 如果大于限制阀值,判断达到阀值的时间,如果不大于[用户访问最小安全时间]则视为恶意访问,跳转到异常页面
        if (ipMap.containsKey(ip)) {
            Long[] ipInfo = ipMap.get(ip);
            ipInfo[0] = ipInfo[0] + 1;
            if (ipInfo[0] > LIMIT_NUMBER) {
                Long ipAccessTime = ipInfo[1];
                Long currentTimeMillis = System.currentTimeMillis();
                // 限制时间内
                if (currentTimeMillis - ipAccessTime <= MIN_SAFE_TIME) {
                    System.out
                            .println(ip + " 在[" + (currentTimeMillis - ipAccessTime) + "]ms内,共访问了[" + ipInfo[0] + "]次");
                    limitedIpMap.put(ip, currentTimeMillis + LIMITED_TIME_MILLIS);
                    forward(request, response, currentTimeMillis + LIMITED_TIME_MILLIS);
                    return;
                } else {
                    initIpVisitsNumber(ipMap, ip);
                }
            }
        } else {
            initIpVisitsNumber(ipMap, ip);
        }
        context.setAttribute("ipMap", ipMap);
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {

    }

    /**
     * @Description 跳转页面
     * @author zhangyd
     * @date 2016年8月17日 下午5:58:43
     * @param request
     * @param response
     * @param remainingTime
     *            剩余限制时间
     * @throws ServletException
     * @throws IOException
     */
    private void forward(HttpServletRequest request, HttpServletResponse response, long remainingTime)
            throws ServletException, IOException {
        request.setAttribute("remainingTime", remainingTime);
        request.getRequestDispatcher("/error/overLimitIP").forward(request, response);
    }

    /**
     * @Description 是否是被限制的IP
     * @author zhangyd
     * @date 2016年8月8日 下午5:39:17
     * @param limitedIpMap
     * @param ip
     * @return true : 被限制  false : 正常
     */
    private boolean isLimitedIP(ConcurrentHashMap<String, Long> limitedIpMap, String ip) {
        if (limitedIpMap == null  limitedIpMap.isEmpty()  ip == null) {
            // 没有被限制
            return false;
        }
        return limitedIpMap.containsKey(ip);
    }

    /**
     * 初始化用户访问次数和访问时间
     * 
     * @author zhangyd
     * @date 2016年7月29日 上午10:01:39
     * @param ipMap
     * @param ip
     */
    private void initIpVisitsNumber(ConcurrentHashMap<String, Long[]> ipMap, String ip) {
        Long[] ipInfo = new Long[2];
        ipInfo[0] = 0L;// 访问次数
        ipInfo[1] = System.currentTimeMillis();// 初次访问时间
        ipMap.put(ip, ipInfo);
    }

}

加入Spring Task定时器。
配置文件:applicationContext.xml

<context:component-scan base-package="com.test" >
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>
<!-- 配置定时任务 -->
<task:annotation-driven /> 

IPFilterTask

package com.test.task;

import java.util.Date;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.ServletContext;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.context.ContextLoaderListener;

/**
 * @Description IP限制过滤器定时任务,过滤受限的IP,剔除已经到期的限制IP
 * @author zhangyd
 * @date 2016年8月18日 上午9:39:19
 * @version V1.0
 * @since JDK : 1.7
 * @modify 将IP限制过滤手动触发改为定时任务
 */
@Component
public class IPFilterTask {

    /**
     * @Description 30s执行一次过滤操作
     * @author zhangyd
     * @date 2016年8月17日 下午5:49:55
     */
    @Scheduled(cron = "0/30 * *  * * ? ")
    public void filterLimitedIpMap() {
        ServletContext context = ContextLoaderListener.getCurrentWebApplicationContext().getServletContext();
        @SuppressWarnings("unchecked")
        ConcurrentHashMap<String, Long> limitedIpMap = (ConcurrentHashMap<String, Long>) context
                .getAttribute("limitedIpMap");
        if (limitedIpMap.isEmpty()) {
            return;
        }
        Iterator<Entry<String, Long>> it = limitedIpMap.entrySet().iterator();
        long currentTimeMillis = System.currentTimeMillis();
        while (it.hasNext()) {
            Entry<String, Long> e = it.next();
            long expireTimeMillis = e.getValue();
            if (expireTimeMillis <= currentTimeMillis) {
                it.remove();
                System.out.println(new Date() + "时,去掉了一个限制用户[" + e.getKey() + "]");
            }
        }
    }
}

有不对的地方,欢迎指正。

我可以对一个人无限的好,前提是值得。 ——慕冬雪

6人推荐
随时随地看视频
慕课网APP

热门评论

private static final long LIMITED_TIME_MILLIS = 60 * 1000;
应该=60 * 60 * 1000;

查看全部评论