通常在普通的操作當中,我們不需要處理重復提交的,而且有很多方法來防止重復提交。比如在登陸過程中,通過使用redirect,可以讓用戶登陸之上重定向到后台首頁界面,當用戶刷新界面時就不會觸發重復提交了。或者使用token,隱藏在表單中,當提交時進行token驗證,驗證失敗也不讓提交。這都是一般的做法。
我們這次碰到的問題是重復提交本身就是一個錯誤,重復提交會導致一些相關數據的邏輯不再正確。而這些重復提交並不是通過普通的刷新界面,或者兩次點擊按鈕來進行的。在普通的操作當中,我們可以通過一系列的手段,使得相應參數被清零,從而防止數據上的不正確。但是,在一種情況下,這些手段都不再有效,那就是並發的重復提交。
並發重復提交,那就是在同一時間內(時間間隔可以縮短到0.X秒之內),在這種情況下,所有的常規邏輯都不再有效,因為多個請求,同時進入系統,系統已不能判斷出這些請求是否是無效的,它們同時通過常規的重復邏輯判斷,並最終在同一時間內將數據寫入到數據庫中,引起數據錯誤。
舉一個簡單的例子,在系統中銷售一個商品,首先通過該商品id進入到系統邏輯判斷,判斷此商品是否已售出,如果未售出,就進行數據存取操作。商品是否售出,是一個邏輯判斷,是驗證數據存儲到數據庫的一道門。在常規的判斷當中,前一請求通過這道門之后,后一請求就不能通過了,因為驗證為false。但在並發請求中,兩個或多個請求同時通過了這道門,因為都是同時進入到判斷,在判斷之前都驗證商品沒有被售出,所以就同時進入到數據的存儲當中。
在常規的java開發中,對於這種情況,臨界資源,通常是使用加鎖來保證這種情況的先后順序。但是加鎖有一個問題即是,它是對於全局信息的加鎖,即對整個將要銷售的商品進行加鎖了。對於BS應用來說,我們必須保證另一個操作人員的同一種商品的銷售請求通過,即只限制同一個操作人員銷售的並發請求,不限制多個操作人員不同請求的處理。
在這種情況下,我們的加鎖就不能簡單的鎖定在商品上,而是要鎖定在與操作人員有關的信息上,這就是session。
session是一個在單個操作人員整個操作過程中,與服務器端保持通信的惟一識別信息。在同一操作人員的多次請求當中,session始終保證是同一個對象,而不是多個對象,因為可以對其加鎖。當同一操作人員多個請求進入時,可以通過session限制只能單向通行。
本文正是通過使用session以及在session中加入token,來驗證同一個操作人員是否進行了並發重復的請求,在后一個請求到來時,使用session中的token驗證請求中的token是否一致,當不一致時,被認為是重復提交,將不准許通過。
原理: 服務器端在處理客戶端的請求之前,會將請求中包含的令牌值與保存在當前會話中的令牌值進行比較,看是否匹配。在處理完該請求后,並且在信息達到客戶端之前,將產生一個新的令牌。該令牌值將會替換當前會話中的令牌值,並且傳到客戶端。這樣如果用戶回退到剛才的提交頁面並再一次提交的話,客戶端傳過來的令牌與服務其中的令牌值不一致,從而有效的防止了提交。 實現: 首先在預添加的Action的execute()方法中創建並保存一個令牌 saveToken(request); 功能:創建一個新令牌值,並且將它保存到當前的session中,如果HttpSession對象不存在的話,就先創建這個對象。 由預添加的Action將令牌傳到了添加的頁面上,作為一個隱藏域。在添加頁面提交給添加AddAction后,在execute()方法中: 先判斷當前會話中的令牌值和請求中的令牌值是不是一致的: isTokenValid(request) 如果不是一致的,給出錯誤信息,並且通過saveToken(request);刷新令牌值。 如果是一致的,那么就執行sql語句保存(添加),再通過resetToken(request)方法刪除當前會話中的令牌。
整個流程可以由如下流程來表述:
- 客戶端申請token
- 服務器端生成token,並存放在session中,同時將token發送到客戶端
- 客戶端存儲token,在請求提交時,同時發送token信息
- 服務器端統一攔截同一個用戶的所有請求,驗證當前請求是否需要被驗證(不是所有請求都驗證重復提交)
- 驗證session中token是否和用戶請求中的token一致,如果一致則放行
- session清除會話中的token,為下一次的token生成作准備
- 並發重復請求到來,驗證token和請求token不一致,請求被拒絕
由以上的流程,我們整個實現需要以下幾個東西
- token生成器,負責生成token
- 客戶token請求處理action,負責處理客戶請求,並返回token信息
- token攔截器,用於攔截指定的請求是否需要驗證token
- token請求攔截標識,用於標識哪些請求是需要被攔截的
- 客戶端token請求處理方法,用於請求token,並存放於特定操作中,並在提交時發送到請求中
token生成器
token生成器在這里使用了一個隨機數來實現,即隨機生成一個數字,即實現token生成,如下所示:
1 private static final Random random = new Random(System.currentTimeMillis()); 2 public static final String TOKENPARAM = "session-token"; 3
4 /** 生成一個token */
5 public static synchronized String generateToken(HttpSession session) { 6 String s = String.valueOf(random.nextLong()); 7 session.setAttribute(TOKENPARAM, s); 8 return s; 9 }
token請求處理action
請求處理action,即接收相應的請求,然后直接返回相對應的token即可,如下即為一個為ajax請求生成token的處理action:
1 public String generateTokenAjax() { 2 String token = SessionTokenGenerator.generateToken(ServletActionContext.getRequest().getSession()); 3 AjaxSupport.sendSuccessText(token); 4 return NONE; 5 }
token請求攔截標識
攔截標識,即表示哪些方法需要被攔截,這里可以使用注解來實現,即在要攔截的方法上追加類似@TokenNeed的注解,或者使用配置文件,將需要攔截的方法列表記錄在配置文件中,在本文中,使用了一個配置文件來記錄
token攔截器
token攔截器實現了我們所需要的攔截處理,在當碰到需要攔截的方法請求中,將同步進行token的判斷和處理,並根據處理結果判斷是否該繼續放行或攔截之:
1 public String intercept(ActionInvocation invocation) throws Exception { 2 String action = invocation.getProxy().getAction().getClass().getName(); 3 String method = invocation.getProxy().getMethod(); 4 final HttpSession session = ServletActionContext.getRequest().getSession(); 5 if(includeMethodSet.contains(action + "." + method)) { 6 synchronized(session) { 7 String paramSessionToken = ServletActionContext.getRequest().getParameter(SessionTokenGenerator.TOKENPARAM); 8 String sessionSessionToken = (String) session.getAttribute(SessionTokenGenerator.TOKENPARAM); 9 if(sessionSessionToken == null || paramSessionToken == null || !paramSessionToken.equals(sessionSessionToken)) 10 return fail(); 11 session.removeAttribute(SessionTokenGenerator.TOKENPARAM); 12 } 13 } 14 return invocation.invoke(); 15 }
如上即是判斷處理的方法是否在攔截列表中,如果是,則取得參數中的token,再將其與session中的token相比,如果不一致,則直接返回fail,隨后將其從session中移除。
客戶端token實現
作為客戶端,只需要在進行請求提交之前申請一個token,在請求時,將此token加到請求中即可。在本文中,有一個jquery的ajax方法來處理token請求,隨后在進行ajax請求時將此token一起加入到param。如下即為token的jquery請求
1 m_ylf.token = function() { 2 m_ylf.invoke("/token/generateToken",{}, function(re) { 3 re = re["result"]; 4 window["session-token"] = re; 5 }); 6 }
即在處理時將接收到的token放到window中,要提交請求時再將其從window中取出,一並提交即可,如下的統一ajax處理方法:
1 //追加session-token
2 if(window["session-token"]) 3 param["session-token"] = window["session-token"];
至此,整個防session Token請求即完成。如果在客戶端模擬多個請求中,首先會有一個請求被成功處理,其它的請求即直接返回類似“不能重復提交”的錯誤警告(對於ajax請求)。