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

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