作者:EasyJF開源團隊 大峽
一、簡介
在Java Web應用程中,特別是網站開發中,我們有時候需要為應用程式增加一個入侵檢測程式來防止惡意重新整理的功能,防止非法使用者不斷的往Web應用中重複發送資料。當然,入侵檢測可以用很多方法實現,包括軟體、硬體防火牆,入侵檢測的策略也很多。在這裡我們主要介紹的是Java Web應用程式中通過軟體的方式實現簡單的入侵檢測及防禦。
該方法的實現原理很簡單,就是使用者訪問Web系統時記錄每個使用者的資訊,然後進行對照,並根據設定的策略(比如:1秒鐘重新整理頁面10次)判斷使用者是否屬於惡意重新整理。
我們的入侵檢測程式應該放到所有Java Web程式的執行前,也即若發現使用者是惡意重新整理就不再繼續執行Java Web中的其它部分內容,否則就會失去了意義。這就需要以外掛程式的方式把入侵檢測的程式置入Java Web應用中,使得每次使用者訪問Java Web,都先要到這個入侵檢測程式中報一次到,符合規則才能允許存取。
Java Web應用大致分為兩種,一種純JSP(+Java Bean)方式,一種是基於架構(如Struts、EasyJWeb等)的。第一種方式的Java Web可以通過Java Servlet中的Filter介面實現,也即實現一個Filter介面,在其doFilter方法中插入入侵檢測程式,然後再web.xml中作簡單的配置即可。在基於架構的Web應用中,由於所有應用都有一個入口,因此可以把入侵檢測的程式直接插入架構入口引擎中,使架構本身支援入侵檢測功能。當然,也可以通過實現Filter介面來實現。
在EasyJWeb架構中,已經置入了簡單入侵檢測的程式,因此,這裡我們以EasyJWeb架構為例,介紹具體的實現方法及源碼,完整的代碼可以在EasyJWeb源碼中找到。
在基於EasyJWeb的Java Web應用中(如http://www.easyjf.com/bbs/),預設情況下你只要連續重新整理頁面次數過多,即會彈出如下的錯誤:
EasyJWeb架構友情提示!:-):
您對頁面的重新整理太快,請等待60秒後再重新整理頁面!
詳細請查詢http://www.easyjf.com
二、使用者訪問資訊記錄UserConnect.java類
這個類是一個簡單的Java Bean,主要代表使用者的資訊,包括使用者名稱、IP、第一次訪問時間、最後登入時間、登入次數、使用者狀態等。全部
代碼如下:
package com.easyjf.web;
import java.util.Date;
/**
*
* <p>Title:使用者驗證資訊</p>
* <p>Description:記錄使用者登入資訊,判斷使用者登入情況</p>
* <p>Copyright: Copyright (c) 2006</p>
* <p>Company: www.easyjf.com</p>
* @author 蔡世友
* @version 1.0
*/
public class UserConnect {
private String userName;
private String ip;
private Date firstFailureTime;
private Date lastLoginTime;
private int failureTimes;//使用者登入失敗次數
private int status=0;//使用者狀態0表示正常,-1表示鎖定
public int getFailureTimes() {
return failureTimes;
}
public void setFailureTimes(int failureTimes) {
this.failureTimes = failureTimes;
}
public Date getFirstFailureTime() {
return firstFailureTime;
}
public void setFirstFailureTime(Date firstFailureTime) {
this.firstFailureTime = firstFailureTime;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public Date getLastLoginTime() {
return lastLoginTime;
}
public void setLastLoginTime(Date lastLoginTime) {
this.lastLoginTime = lastLoginTime;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
}
三、監控線程UserConnectManage.java類
這是入侵檢測的核心部分,主要實現具體的入侵檢測、記錄、判斷使用者資訊、線上使用者的重新整理等功能,並提供其它應用程式使用本組件的調用介面。
package com.easyjf.web;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.apache.log4j.Logger;
/**
*
* <p>Title:使用者入侵檢測資訊</p>
* <p>Description:用於判斷使用者重新整理情況檢查,預設為10秒鐘之內連續串連10次為逾時</p>
* <p>Copyright: Copyright (c) 2006</p>
* <p>Company: www.easyjf.com</p>
* @author 蔡世友
* @version 1.0
*/
public class UserConnectManage {
private static final Logger logger = (Logger) Logger.getLogger(UserConnectManage.class.getName());
private static int maxFailureTimes=10;//最大登入失敗次數
private static long maxFailureInterval=10000;//毫秒,達到最大登入次數且在這個時間範圍內
private static long waitInterval=60000;//失敗後接受串連的等待時間,預設1分鐘
private static int maxOnlineUser=200;//同時線上的最大數
private final static Map users=new HashMap();//使用ip+userName為key存放使用者登入資訊UserLoginAuth
private static Thread checkThread=null;
private static class CheckTimeOut implements Runnable{
private Thread parentThread;
public CheckTimeOut(Thread parentThread)
{
this.parentThread=parentThread;
synchronized(this){
if(checkThread==null){
checkThread= new Thread(this);
//System.out.println("建立一個新線程!");
checkThread.start();
}
}
}
public void run() {
while(true)
{
if(parentThread.isAlive()){
try{
Thread.sleep(2000);
int i=0;
if(users.size()>maxOnlineUser)//當達到最大使用者數時清除
{
synchronized(users){//執行刪除操作
Iterator it=users.keySet().iterator();
Set set=new HashSet();
Date now=new Date();
while(it.hasNext())
{
Object key=it.next();
UserConnect user=(UserConnect)users.get(key);
if(now.getTime()-user.getFirstFailureTime().getTime()>maxFailureInterval)//刪除逾時的使用者
{
set.add(key);
logger.info("刪除了一個逾時的串連"+i);
i++;
}
}
if(i<5)//如果刪除少於5個,則強行刪除1/2線上記錄,犧牲效能的情況下保證記憶體
{
int num=maxOnlineUser/2;
it=users.keySet().iterator();
while(it.hasNext() && i<num)
{
set.add(it.next());
logger.info("刪除了一個多餘的串連"+i);
i++;
}
}
users.keySet().removeAll(set);
}
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
else
{
break;
}
}
logger.info("監視程式運行結束!");
}
}
//通過checkLoginValidate判斷是否合法的登入串連,如果合法則繼續,非法則執行
public static boolean checkLoginValidate(String ip,String userName)//只檢查認證失敗次數
{
boolean ret=true;
Date now=new Date();
String key=ip+":"+userName;
UserConnect auth=(UserConnect)users.get(key);
if(auth==null)//把使用者當前的訪問資訊加入到users容器中
{
auth=new UserConnect();
auth.setIp(ip);
auth.setUserName(userName);
auth.setFailureTimes(0);
auth.setFirstFailureTime(now);
users.put(key,auth);
if(checkThread==null)new CheckTimeOut(Thread.currentThread());
}
else
{
if(auth.getFailureTimes()>maxFailureTimes)
{
//如果在限定的時間間隔內,則返回拒絕使用者串連的資訊
if((now.getTime()-auth.getFirstFailureTime().getTime())<maxFailureInterval)
{
ret=false;
auth.setStatus(-1);
}
else if(auth.getStatus()==-1 && (now.getTime()-auth.getFirstFailureTime().getTime()<(maxFailureInterval+waitInterval)))//重設計數器
{
ret=false;
}
else
{
auth.setFailureTimes(0);
auth.setFirstFailureTime(now);
auth.setStatus(0);
}
}
//登入次數加1
auth.setFailureTimes(auth.getFailureTimes()+1);
}
//System.out.println(key+":"+auth.getFailureTimes()+":"+ret+":"+(now.getTime()-auth.getFirstFailureTime().getTime()));
return ret;
}
public static void reset(String ip,String userName)//重設使用者資訊
{
Date now=new Date();
String key=ip+":"+userName;
UserConnect auth=(UserConnect)users.get(key);
if(auth==null)//把使用者當前的訪問資訊加入到users容器中
{
auth=new UserConnect();
auth.setIp(ip);
auth.setUserName(userName);
auth.setFailureTimes(0);
auth.setFirstFailureTime(now);
users.put(key,auth);
}
else
{
auth.setFailureTimes(0);
auth.setFirstFailureTime(now);
}
}
public static void remove(String ip,String userName)//刪除使用者在容器中的記錄
{
String key=ip+":"+userName;
users.remove(key);
}
public static void clear()//清空容器中內容
{
if(!users.isEmpty())users.clear();
}
public static long getMaxFailureInterval() {
return maxFailureInterval;
}
public static void setMaxFailureInterval(long maxFailureInterval) {
UserConnectManage.maxFailureInterval = maxFailureInterval;
}
public static int getMaxFailureTimes() {
return maxFailureTimes;
}
public static void setMaxFailureTimes(int maxFailureTimes) {
UserConnectManage.maxFailureTimes = maxFailureTimes;
}
public static int getMaxOnlineUser() {
return maxOnlineUser;
}
public static void setMaxOnlineUser(int maxOnlineUser) {
UserConnectManage.maxOnlineUser = maxOnlineUser;
}
public static long getWaitInterval() {
return waitInterval;
}
public static void setWaitInterval(long waitInterval) {
UserConnectManage.waitInterval = waitInterval;
}
四、調用介面
在需要進入侵檢測判斷的地方,直接使用UserConnectManage類中的checkLoginValidate方法即可。如EasyJWeb的核心Servlet
com.easyjf.web.ActionServlet中調用UserConnectManage的代碼:
if(!UserConnectManage.checkLoginValidate(request.getRemoteAddr(),"guest"))
{
info(request,response,new Exception("您對頁面的重新整理太快,請等待"+UserConnectManage.getWaitInterval()/1000+"秒
後再重新整理頁面!"));
return;
}
五、總結
當然,這裡提供的方法只是一個簡單的實現樣本,由於上面的使用者資訊是直接儲存在記憶體中,若並發使用者很大的時候的代碼的佔用,可以考慮引入資料庫來記錄使用者的訪問資訊,當然相應的執行效率肯定用降低。上面介紹的實現中,入侵檢測判斷的策略也只有使用者訪問次數及時間間隔兩個元素,您還可以根據你的實現情況增加其它的檢測元素。
由於水平有限,該設計上有N不合理或者需要改進的地方,懇請大家指正!