前几天在博客园看到一篇文章,讲述了某位程序猿收到银行卡诈骗短信,要求登录所谓的银行网站查询银行卡状态。很显然,骗子是想非法记录用户的银行卡卡号和密码来从事诈骗活动。或许大多数人只是一笑而过,当作垃圾短信给删除了。程序猿大部分都是闲得无聊,没事瞎折腾,这位园友自己写了个小程序,循环向该网站提交数据,结果大家都懂的,把别人网站搞崩了。<在这里也算是为民除害吧--仅代表个人观点>
其实鄙人也曾有过这样的想法,但没有实施,在此思过,也对那些孜孜不倦编程,写博客,答疑解惑的各位园友表示钦佩和感谢,向你们看齐。
联想到一直处于众矢之的深陷购票风波的12306,心里有些想法,不吐不快,欢迎吐槽。
用没装抢票插件的浏览器打开12306的购票页面,每次点击蓝色的查询按钮后,按钮变灰无法点击,6秒钟之后恢复,可在此查询,很显然前端使用JS进行了处理。后来360,搜狗,猎豹浏览器都退出了自己的抢票插件,绕过了12306 JS控制,每次查询后立马可在次查询,一下子受到网友们的热捧,成为人人必备的抢票利器。<我一次都没抢到自己想要的票,够霉的吧!>
这时,12306又跳出来了,约谈企业什么云云。再后来,使用抢票插件自动提交订单时没有任何提示,直接跳到登录页面,够让人崩溃的吧。
我是中华人民共和国守法公民,不在此作过多评价,只谈谈我从网站设计方面的想法,抛砖引玉,欢迎讨论。
首先,对数据的校验不能仅仅依靠前段,后台也应有相应的逻辑处理,防止非法操作,以保证数据的安全性和体统的可靠性。
其次,在数据校验后,应当有相应的提示,特别是校验不通过时及时给用户以反馈,提升用户体验。
下面使用Struts2开发小web模拟后台数据校验,防止同一IP重复恶意提交数据。
- 1. Struts.xml配置
<package name="default" extends="struts-default">
<default-action-ref name="index"/>
<action name="index" >
<result type="redirectAction">
<param name="namespace">/Main</param>
<param name="actionName">login_input</param>
</result>
</action>
</package>
以上表示网站默认登录页,显示Login.jsp
<package name="Main" namespace="/Main" extends="struts-default">
<action name="login_*" class="com.struts.action.LoginAction" method="{1}">
<result name="input">/Login.jsp</result>
<result name="success" type="redirectAction">main</result>
</action>
<action name ="error">
<result>/Error.jsp</result>
</action>
</package>
以上表示发生错误时显示Error.jsp
说明:姑且对用户登录进行校验,如果短期内大量重复提交数据尝试登录就认为是恶意行为,30分钟内不允许登录,所提交的数据被拦截不会提交至数据库验证,直接导向至错误提示界面。
下面是错误提示界面,Error.jsp
<%@ page language="java" contentType="text/html; charset=GB18030"
pageEncoding="GB18030"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=GB18030">
<title>error</title>
</head>
<body>
<p>由于你多次重复提交数据,被列为非法操作,请半小时后再尝试访问!</p>
<span style="color:red;">如有疑问,请联系管理员xxxxx!</span>
</body>
</html>
2. Web.xml配置filter
<filter>
<filter-name>IPFilter</filter-name>
<filter-class>com.struts.filter.IPFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>IPFilter</filter-name>
<url-pattern>/Main/login_login.action</url-pattern>
</filter-mapping>
以上表示对用户登录进行校验,在com.struts.filter.IPFilter类中进行处理。
3. com.struts.filter.IPFilter
思路:记录每个IP访问的时间,次数,状态,这里时间记录第一次访问和最近一次访问的时间,次数为这段时间间隔内的访问次数,需要自定义对象进行封装。如果第一分钟内请求超过30次,或平均每分钟超过15次,IP为无效。<为什么不直接计算平均每分钟请求的次数,而要分为一分钟以内和大于一分钟呢?如果我点击的频率很高,但是只点击几次登陆,有可能频率超过15次/分>。每接收到客户端请求时,判断是否是全新的请求,如果是全新请求,将该IP和访问时间记录下来存入容器,否则判断距离上次访问是否超过半小时,再判断状态是否有效,再判断请求频率。
部分代码:
package com.struts.filter;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.FilterConfig;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import java.util.HashMap;
import java.util.Map;
public class IPFilter implements Filter {
IPContainer IPCon = null;
public void destroy() {
IPCon = null;
}
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) req;
String ClientIP = getIPAddress(httpReq);//获取IP
System.out.println(ClientIP);
Visiter visiter = new Visiter();
visiter.setLatestlogin(System.currentTimeMillis());
if (!IPCon.CheckVisiterIP(ClientIP, visiter)) {
//校验IP
((HttpServletResponse)res).sendRedirect("error");
} else {
chain.doFilter(req, res);
}
}
public void init(FilterConfig filterconfig) throws ServletException {
IPCon = new IPContainer();
}
public String getIPAddress(HttpServletRequest req) {
String ip = req.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = req.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = req.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = req.getRemoteAddr();
}
return ip;
}
private class IPContainer {
Map<String, Visiter> visiterMap;
IPContainer() {
visiterMap = new HashMap<String, Visiter>();
}
boolean CheckVisiterIP(String IP, Visiter visiter) {
boolean validate = false;
if (visiterMap.containsKey(IP)) {
//不是全新请求
Visiter vis = visiterMap.get(IP);
if (visiter.getLatestlogin() - vis.getLatestlogin() > 1800000) {
// 距离上次访问超过30分钟
vis.setFirstlogin(visiter.getLatestlogin());
vis.setLatestlogin(visiter.getLatestlogin());
vis.setLogintimes(1L);
vis.setState(0);
validate = true;
} else if (vis.getState() == 1) {
// IP 无效
validate = false;
} else if ((visiter.getLatestlogin() - vis.getFirstlogin() < 60000 && vis
.getLogintimes() > 30)
|| (visiter.getLatestlogin() - vis.getFirstlogin() > 60000 && vis
.getLogintimes()
* 60000
/ (visiter.getLatestlogin() - vis
.getFirstlogin()) > 15L)) {
// 第一分钟内请求超过三十次,或平均每分钟请求次数多余15次,当作非法请求
vis.setLatestlogin(visiter.getLatestlogin());
vis.setState(1);
validate = false;
} else {
//请求处于正常频率,合法
vis.setLatestlogin(visiter.getLatestlogin());
vis.incrLoginTimes();
validate = true;
}
} else {
//第一次访问
visiter.setFirstlogin(visiter.getLatestlogin());
visiter.incrLoginTimes();
visiterMap.put(IP, visiter);
validate = true;
}
return validate;
}
}
class IPAudit {
//略,监控Container,移除长时间不访问的IP,释放内存
}
class Visiter {
private long firstlogin; // 第一次请求时间
private long latestlogin; // 最近请求时间
private long logintimes; // 请求次数
private int state; // 状态,0 有效,1 无效
Visiter() {
firstlogin = 0L;
latestlogin = 0L;
logintimes = 0L;
setState(0);
}
//略,get set
public void incrLoginTimes() {
logintimes += 1;
}
}
}