防止惡意刷新頁面的Java實(shí)現(xiàn)
在很多對安全性要求較高的項(xiàng)目中,需要提供至少一種對整個(gè)項(xiàng)目的安全控制方案,常用的比如身份認(rèn)證、訪問控制、安全審計(jì)等等。由于設(shè)計(jì)不合理而導(dǎo)致的安全問題可能會給項(xiàng)目帶來非常大的隱患,正是因?yàn)槿绱?,安全問題也得到了廣大web項(xiàng)目開發(fā)者的重視,尤其是在電子政務(wù)和電子商務(wù)的開發(fā)中,更是需要提供一定層次上的安全性要求。
本文的重點(diǎn)在于實(shí)現(xiàn)一種防止惡意刷新頁面的方法,筆者在實(shí)現(xiàn)該功能時(shí),查找了很多資料并且進(jìn)行了多次討論協(xié)商,提出了這種針對特定需求的,獨(dú)立性較高的方案。
下面,我們來具體說一下客戶的需求。
該功能是在某電子政務(wù)的安全審計(jì)子系統(tǒng)中要求實(shí)現(xiàn)的一個(gè)功能,因?yàn)榘踩珜徲?jì)子系統(tǒng)涉及多方面的內(nèi)容,包括外網(wǎng)訪問日志,專網(wǎng)和內(nèi)網(wǎng)辦公日志,數(shù)據(jù)庫操作日志等等的控制和查詢等等,通過這些功能,可以提供給管理員各種接口,從而可以查看系統(tǒng)的使用狀況,并對誤操作或者惡意操作進(jìn)行有效追蹤和審計(jì)。而外網(wǎng)作為電子政務(wù)的門戶,是為廣大公眾提供訪問的接口,那么保證其正常瀏覽是作為電子政務(wù)系統(tǒng)中不可或缺的功能需求,因此,實(shí)現(xiàn)防止惡意刷新的功能便是提供良好功能保障的眾多手段中的一種。
這里所說的惡意刷新并不是指網(wǎng)絡(luò)上通常所指的如何通過javascript腳本來屏蔽F5鍵等方案,而是通過過濾機(jī)制,由程序?qū)崿F(xiàn)的一種對某次會話中的惡意訪問的控制,該方案中制定的規(guī)則是:來自某一客戶的訪問如果在10秒中內(nèi)超過10次則被假設(shè)為惡意訪問。這樣的方案也是比較合理的,既避免了惡意用戶使用F5鍵來頻繁刷新某一固定頁面,也防止了用戶漫無目的地亂點(diǎn)頁面。同時(shí),該方案還提供了輔助功能,在規(guī)則設(shè)定方面比較靈活,默認(rèn)為10次/10秒,這兩個(gè)參數(shù)是可以通過程序來設(shè)置的;在惡意控制方面,如果用戶的訪問違反了該規(guī)則,則通過過濾器機(jī)制提取該用戶的IP地址,將其置為拒絕提供服務(wù)的IP列表中,將拒絕再次為來自該IP的請求提供服務(wù)。
下面,我們來具體說明一下該方案的實(shí)現(xiàn)方法。
首先,從整個(gè)方案的實(shí)施體系來講,我們提取出如下的控制流程。

從上圖可以看出,來自客戶端的請求首先要經(jīng)過IP過濾器的過濾,只有不在惡意IP列表中的IP地址的請求才有可能被響應(yīng),然后還要經(jīng)過惡意刷新過濾器的驗(yàn)證才能得到服務(wù)器的最終響應(yīng)。如果該IP在10秒內(nèi)連續(xù)請求的次數(shù)達(dá)到了10次,那么,它將被記入到惡意IP列表中,將不會通過IP過濾器的驗(yàn)證,不會得到服務(wù)器的響應(yīng),從而,我們就實(shí)現(xiàn)了對惡意用戶的過濾。當(dāng)然,通過IP過濾器的作用,我們還可以將某些IP直接列入黑名單中,比如某些具有攻擊性的網(wǎng)站的IP地址或者曾經(jīng)通過網(wǎng)絡(luò)入侵檢測軟件的診斷,將危險(xiǎn)IP也加入黑名單,這樣,可以提高我們的應(yīng)用的安全性和可用性。
接下來,我們通過代碼來看一下如何具體實(shí)現(xiàn)我們的功能。通過上面的流程圖,我們可以清晰看到,在應(yīng)用中,我們配置了兩個(gè)過濾器(關(guān)于過濾器的原理和實(shí)現(xiàn),請讀者參考相關(guān)資料),當(dāng)然,我們也可以將其寫在一個(gè)過濾器的doFilter()方法中。
本文方案的實(shí)現(xiàn)中采用了兩個(gè)過濾器,下面我們來簡單看一下IP過濾器。以下是具體的代碼實(shí)現(xiàn),通過代碼中的注釋,可以清楚看到我們的實(shí)現(xiàn)思路。
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse res = (HttpServletResponse)response;
String ip = request.getRemoteAddr(); //得到客戶端IP地址
if(!req.getRequestURI().toUpperCase().equals("ERR.JSP")) ...{
IP ipControl = new IP(); //生成一個(gè)IP類的事例
try ...{
// getDangerousIP(ip)將查詢指定IP是否在IP黑名單的數(shù)據(jù)庫中
if((ip != null )&&(ipControl.getDangerousIP(ip) !=0)) ...{
res.sendRedirect("/err.jsp?errmsg=ip");
} else ...{
//如果該IP安全,則繼續(xù)執(zhí)行
chain.doFilter(request, response);
}
} catch (ServletException ex) ...{
ex.printStackTrace();
} catch (IOException ex) ...{
ex.printStackTrace();
} catch (Exception ex) ...{
ex.printStackTrace();
}
finally...{
ipControl.closeConn();//關(guān)閉數(shù)據(jù)庫連接
}
下面,我們來著重討論如何實(shí)現(xiàn)惡意刷新的過濾器。因?yàn)槲覀兊男枨笫窃谌魏蔚?0秒中記錄請求數(shù)而不是在以10秒為一個(gè)時(shí)間段,那么就需要保證時(shí)間的連續(xù)性,鑒于此需求,我們需要保存用戶連續(xù)10次請求的時(shí)間,如果其最后一次的請求時(shí)間與第一次的時(shí)間差小于10秒并且次數(shù)已經(jīng)達(dá)到10次,則違背設(shè)定規(guī)則,我們設(shè)計(jì)了下面的類來保存用戶的訪問時(shí)間序列。
public class ArrayTime ...{
private long[] time;
private int length = 10; //默認(rèn)為十次(10s內(nèi)刷新10次則違反規(guī)則)
public ArrayTime() ...{
}
public void init() ...{
time = new long[length];
}
public int getLength() ...{
return this.length;
}
public void setLength(int len)
...{
this.length = len;
}
public long getLast() ...{
return this.time[length-1];
}
public long getFirst() ...{
return this.time[0];
}
public long getElement(int i) ...{
return time;
}
public void insert(long nextTime) ...{
if (this.getLast() != 0)//數(shù)組已經(jīng)滿了
...{
//去掉首元素,將數(shù)組元素順序前移,nextTime插到最后
for(int i = 0 ;i < this.length-1;i++) ...{
time = time[i+1];
}
this.time[length-1] = nextTime;
} else ...{
//插到下一個(gè),不用排序
int j=0;
while(time[j] != 0) ...{
j++;
}
time[j] = nextTime;
}
}
}
這里要注意的是,因?yàn)槲覀優(yōu)楣芾韱T提供了規(guī)則設(shè)置的接口,所以保存時(shí)間序列的數(shù)組長度是可設(shè)定的。下面是來自客戶端的訪問者類的實(shí)現(xiàn):
public class Visitor ...{
/**//* Creates a new instance of Visitor
*外網(wǎng)訪問者,以sessionID作為標(biāo)識
*違反訪問規(guī)則將其IP列為受限IP,拒絕訪問
*/
private String sessionID = null;
private ArrayTime requestTimeQueue= new ArrayTime();
public Visitor() ...{
}
public void setSessionID(String sessionID)
...{
this.sessionID = sessionID;
}
public String getSessionID()
...{
return this.sessionID;
}
public void setRequestTimeQueue(ArrayTime requestTimeQueue)
...{
this.requestTimeQueue = requestTimeQueue;
}
public ArrayTime getRequestTimeQueue()
...{
return this.requestTimeQueue;
}
}
該類中采用了訪問者的會話ID來標(biāo)識來自客戶端的請求,讀者可以很方便地修改該標(biāo)識,比如修改為注冊會員的用戶名,這樣,就可以實(shí)現(xiàn)對會員的惡意訪問的屏蔽,同時(shí)這樣的好處還在于,可以屏棄對session和cookies的依賴,因?yàn)槿绻脩魹g覽器如果禁用cookie,通常利用session和cookies實(shí)現(xiàn)的惡意刷新就失去了作用。雖然,我們這樣做也可能是一種消耗內(nèi)存的方式,但是,的確是一種值得采用并進(jìn)行優(yōu)化的折中方案。
最后,也是最關(guān)鍵的就是我們?nèi)绾蝸硗ㄟ^過濾器實(shí)現(xiàn)對惡意用戶的請求進(jìn)行屏蔽。下面是該過濾器的doFliter()方法的核心部分的實(shí)現(xiàn)。
//---------------防止惡意刷新的過濾器--------
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse res = (HttpServletResponse)response;
String sessionID = ((HttpServletRequest)request).getSession().getId();//會話ID
Date now =new Date();
Visitor vis = (Visitor) visitors.get(sessionID);//通過sessionID查找訪問者,
if(vis!= null)//找到訪問者,則說明該用戶為再次訪問
...{
//小于10秒,但訪問超過10次
vis.getRequestTimeQueue().insert(now.getTime());//插入當(dāng)前請求時(shí)間
//得到最后一次和第一次的訪問時(shí)間差
Long span = vis.getRequestTimeQueue().getLast() - vis.getRequestTimeQueue().getFirst();
if(span < interval && vis.getRequestTimeQueue().getLast() != 0) ...{
//將該用戶加入黑名單
IP ip = new IP();
ip.setIP(request.getRemoteAddr());
//得到當(dāng)前時(shí)間
Calendar cal = Calendar.getInstance();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time=formatter.format(cal.getTime());
ip.setComments("刷新太快,IP已經(jīng)被封鎖");
try ...{
ip.insertIP();
res.sendRedirect("err.jsp?errmsg=refresh");
return;
}catch(Exception e) ...{
e.printStackTrace();
}finally...{
ip.closeConn();}
}
} else ...{
//當(dāng)前訪問者為初次訪問
ArrayTime timeQueue = new ArrayTime();
timeQueue.setLength(maxCount);
timeQueue.init();
vis=new Visitor();
vis.setSessionID(sessionID);
vis.setRequestTimeQueue(timeQueue);
vis.getRequestTimeQueue().insert(now.getTime());
visitors.put(sessionID,vis);
}
本文中的實(shí)現(xiàn)方案為管理員提供了靈活的接口,可以根據(jù)實(shí)際需要設(shè)置相應(yīng)規(guī)則,刷新過濾器中的兩個(gè)參數(shù)都是可設(shè)置的,比如:
//以下兩個(gè)值可從文件或數(shù)據(jù)庫設(shè)置,從而達(dá)到參數(shù)的可設(shè)置
private static long interval= GlobalConfig.getInt("refresh.interval",10000); //默認(rèn)10秒鐘
private static int maxCount = GlobalConfig.getInt("refresh.count",10);//默認(rèn)最大訪問次數(shù)
該方案的實(shí)現(xiàn)中采用了保存時(shí)間序列的方案,而這樣,就要在內(nèi)存中開辟一個(gè)可變可控的數(shù)組,在一定程度上浪費(fèi)了資源,但是,也提供了在保證連續(xù)時(shí)間下防止惡意刷新的功能,并且可以屏棄對session和cookies的依賴,達(dá)到完全的可自控,保證了有效性。同時(shí),提供了IP過濾機(jī)制來保證功能體系的完善和有效,目前該方案在項(xiàng)目中應(yīng)用良好。如果讀者對此有更加適合的解