1
火车票抢票软件大家都知道,相信大多数人都用过,什么智行,携程,飞猪,都有抢票功能,尤其是在节假日,大家为了抢票,纷纷各种转发,加钱,升级自己的抢票级别,其实背后的机制就是疯狂的调12306的订票接口,调的频率高,抢到票的几率就高,12306也发表声明,这种疯狂调接口的平台12306已经做了限制。
做限制,无外乎就是限制ip访问频率,要不就是控制接口最大并发量,有些恶意攻击就是疯狂调接口,把你接口调的欲哭无泪,下面就来写一个限制ip访问的机制。
2
这个限制ip访问机制就是通过控制ip一段时间访问次数如果超过这个次数,就将该ip拉入黑名单一段时间,等限制时间结束,再移出黑名单。
首先我们创建一个监听器,这个监听器的作用就是在初始化项目时进行一些基础信息的创建。
@JAVAx.servlet.annotation.WebListener public class WebListener implements ServletContextListener { public void contextInitialized(ServletContextEvent servletContextEvent) { Logger logger = LoggerFactory.getLogger(WebListener.class); logger.info("Project初始化成功"); ServletContext context = servletContextEvent.getServletContext(); // IP存储器 Map<String, Long[]> ipMap = new ConcurrentHashMap<String, Long[]>(); context.setAttribute("ipMap", ipMap); // 限制IP存储器:存储被限制的IP信息 Map<String, Long> limitedIpMap = new HashMap<String, Long>(); context.setAttribute("limitedIpMap", limitedIpMap); logger.info("ipmap:"+ipMap.toString()+";limitedIpMap:"+limitedIpMap.toString()+"初始化成功。。。。。"); } public void contextDestroyed(ServletContextEvent servletContextEvent) { } }
这个监听器的作用就是初始化一个Ip存储器和限制IP存储器,Ip存储器就是将请求来的ip进行存储,限制IP存储器就是将被拉入黑名单的ip进行存储。
Ip存储器创建好之后就是要创建过滤器,我们实现ip限制就是依靠过滤器进行实现,创建一个过滤器,设置对所有请求进行拦截。
@WebFilter(urlPatterns = "/*") public class IpFilter implements Filter {
这个@WebFilter注解就是设置拦截器的拦截范围
接着我们就主要是在doFilter方法里实现我们的ip限制功能,ip限制通过默认限制时间,ip连续访问阀值,用户最小访问安全时间这三点进行灵活控制。
public class IpFilter implements Filter { /** * 默认限制时间(单位:ms) */ private static final long LIMITED_TIME_MILLIS = 60 * 1000; /** * 用户连续访问最高阀值,超过该值则认定为恶意操作的IP,进行限制 */ private static final int LIMIT_NUMBER = 5; /** * 用户访问最小安全时间,在该时间内如果访问次数大于阀值,则记录为恶意IP,否则视为正常访问 */ private static final long MIN_SAFE_TIME = 60 * 1000; private FilterConfig config; public void init(FilterConfig config) throws ServletException { this.config = config; }
public class IpFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ServletContext context = config.getServletContext(); // 获取限制IP存储器:存储被限制的IP信息 Map<String, Long> limitedIpMap = (Map<String, Long>) context.getAttribute("limitedIpMap"); // 过滤受限的IP filterLimitedIpMap(limitedIpMap); /** * @param limitedIpMap * @Description 过滤受限的IP,剔除已经到期的限制IP */ 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(); } } }
首先我们需要获取ip存储器,然后对当前的限制ip进行过滤,看是否有已经超过默认时间的ip,将其从限制ip中移除。
// 获取用户IP String ip = IpUtil.getIpAddr(request); System.err.println("ip:" + ip); // 判断是否是被限制的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); System.err.println("ip访问过于频繁:" + ip + "当前时间" + System.currentTimeMillis()); return; }
/** * @param limitedIpMap * @param ip * @return true : 被限制 | false : 正常 * @Description 是否是被限制的IP */ 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; }
接着就要判断当前访问的ip是否在限制Ip名单内,如果在,就进行拦截,不让其进行访问。
// 获取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); System.err.println("ip访问过于频繁:" + ip); request.getRequestDispatcher("/views/error.jsp").forward(request, response); return; } else { //初始化用户访问次数和访问时间 initIpVisitsNumber(ipMap, ip); } } //当ip访问时间与上一次访问时间相隔一分钟时,对该ip的访问次数,和时间进行重置 updateCurrentTimeIp(ipMap, ip); } else { //初始化用户访问次数和访问时间 initIpVisitsNumber(ipMap, ip); System.out.println("您首次访问该网站"); } context.setAttribute("ipMap", ipMap); chain.doFilter(request, response);
/** * 初始化用户访问次数和访问时间 * * @param ipMap * @param ip */ private void initIpVisitsNumber(Map<String, Long[]> ipMap, String ip) { Long[] ipInfo = new Long[3]; ipInfo[0] = 0L;// 访问次数 ipInfo[1] = System.currentTimeMillis();// 初次访问时间 ipInfo[2] = System.currentTimeMillis(); ipMap.put(ip, ipInfo); }
/** * @param updateCurrentTimeIp * @Description 当ip访问时间与上一次访问时间相隔一分钟时,对该Ip的访问次数,和时间进行重置 */ private void updateCurrentTimeIp(Map<String, Long[]> ipMap, String ip) { Long[] ipInfo = ipMap.get(ip); Long ipAccessTime = ipInfo[2]; Long currentTimeMillis = System.currentTimeMillis(); if (ipAccessTime != 0 && (currentTimeMillis - ipAccessTime) >= MIN_SAFE_TIME) { System.out.println("进来了"); Long[] longs = ipMap.get(ip); longs[0] = 0l; longs[1] = System.currentTimeMillis(); longs[2] = System.currentTimeMillis(); } }
最后就是要判断该存储器中有没有当前ip,没有就要存到ip存储器中,有的话就要根据当前ip访问次数和到达阀值时间进行判断是否需要拦截,这里有个ipUtil代码我没放上来,我现在方桑来,方便大家直接拿去用,是一个简单获取访问ip的工具类。
/* * ip访问限制,限制ip每分钟访问次数 * * * */ public class IpUtil { public static String getIpAddr(ServletRequest servletRequest) { HttpServletRequest request=(HttpServletRequest) servletRequest; String ipAddress = null; try { ipAddress = request.getHeader("x-forwarded-for"); if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("WL-Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getRemoteAddr(); if (ipAddress.equals("127.0.0.1")) { // 根据网卡取本机配置的IP InetAddress inet = null; try { inet = InetAddress.getLocalHost(); } catch (Exception e) { e.printStackTrace(); } ipAddress = inet.getHostAddress(); } } // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length() // = 15 if (ipAddress.indexOf(",") > 0) { ipAddress = ipAddress.substring(0, ipAddress.indexOf(",")); } } } catch (Exception e) { ipAddress=""; } // ipAddress = this.getRequest().getRemoteAddr(); return ipAddress; } }
启动项目进行测试访问
您首次访问该网站 ip:192.168.137.1 当前第[2]次访问 当前第[3]次访问 ip:192.168.137.1 ip:192.168.137.1 当前第[4]次访问 当前第[5]次访问 ip:192.168.137.1 ip:192.168.137.1 ip访问过于频繁:192.168.137.1 当前第[6]次访问 ip:192.168.137.1 ip访问过于频繁:192.168.137.1当前时间1560790500409
通过上述步骤,我们的ip限制访问功能就已经实现了,控制ip访问其实在很多公司项目中都是会用到的,尤其是B2C,C2C这样的项目,ip访问拦截可以有效防止恶意攻击,我这个写的也是相对比较简单的,在应对高并发,微服务,负载集群项目时还需要进行改进,尤其在ip存储和限制ip存储可以考虑用redis进行实现,后期如果有时间我也会将其扩展成更实用更符合现在主流项目形式的ip访问限制功能。