在Web開發中表單的重復提交是很嚴重的問題,重復提交成功會產生垃圾數據消耗不必要的資源,更嚴重的是如果遇到惡意刷庫的情況垃圾數據更是數不勝數。在正常使用過程中產生重復提交的情況也有多重情況:鼠標連擊、回退提交、刷新提交、網絡延遲用戶重復提交等。
防止重復提交的方法分兩大類就是客戶端、服務端(這是廢話了)。客戶端主要是用js對按鈕的限制,一次點擊後屏蔽按鈕或者是直接跳轉等待頁面,服務端思路為客戶端加token進行驗證。客戶端就不做詳細介紹,主要介紹服務端的控制。
就是在客戶端不同的地方存儲兩個token,在服務端進行校驗。在Form表單中存儲一個token利用隱藏域,在Cookie中存儲一個(也可以都放到form表單中兩個不同的隱藏域)。檔form表單提交的時候,對這兩個token進行驗證,相同則允許提交否則阻止提交。
優點:
不占用服務器資源
實施起來簡單,易上手
缺點:
容易偽造(防君子不防小人)
占用網絡資源(或許不是那麼明顯)
詳細介紹一下客戶端分布存儲在Form表單中和Cookie中的情況。
客戶端的實現如下:
package cn.simple.token; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * 雙客戶端驗證 * @author ldm * @Date 2016年6月16日 */ @Service("clientTokenProcesser") public class ClientTokenProcesser extends TokenProcesser { @Autowired HttpServletResponse response; @Override public boolean validToken(HttpServletRequest request) { String formToken = request.getParameter(getTokenField()).toString(); System.out.println("formToken:"+formToken); if(StringUtils.isEmpty(formToken)) { printException("表單中沒有token"); return false; } Cookie[] cookies = request.getCookies(); if(cookies==null) { printException("cookie 中沒有token"); } for (Cookie cookie : cookies) { if(cookie.getName().equals(getTokenKey(request))) { String cookieValue = cookie.getValue(); System.out.println("cookieToken:"+cookieValue); if(cookieValue.equals(formToken)) { return true; } } } return false; } private void printException(String msg) { Exception e= new RuntimeException(msg); e.printStackTrace(); } @Override public String getTokenKey(HttpServletRequest request) { String cookieKey = getTokenField() + "_cookie"; return cookieKey; } @Override public void saveToken(HttpServletRequest request) { String token = MakeToken.getInstance().getToken(); request.setAttribute(getTokenField(), token); if (response == null) { throw new RuntimeException("HttpServletResponse is null"); } Cookie cookie = new Cookie(getTokenKey(request), token); response.addCookie(cookie); } @Override public String getClientToken(HttpServletRequest request) { Object token = request.getParameter(getTokenField()); if (token == null) { return null; } else { return token.toString(); } } }View Code
客戶端和服務端的token各自獨立存儲,客戶端存儲在Cookie或者Form的隱藏域(放在Form隱藏域中的時候,需要每個表單)中,服務端存儲在Session(單機系統中可以使用)或者其他緩存系統(分布式系統可以使用)中。
優點:
安全性高(幾乎是無法偽造的)
網絡資源相對於前者有所減少
缺點:
整個系統實施起來較第一種方法的時候復雜度增加
詳細介紹一下服務端存儲在session中客戶端存儲在Cookie中
SessionTokenProcesser實現如下:
package cn.simple.token; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * 服務端用Session存儲 * * @author ldm * @Date 2016年6月16日 */ @Service("sessionTokenProcesser") public class SessionTokenProcesser extends TokenProcesser { @Autowired HttpServletResponse response; @Override public boolean validToken(HttpServletRequest request) { String clientToken = getClientToken(request); if (StringUtils.isEmpty(clientToken)) { return false; } HttpSession session = request.getSession(false); if (session == null) { return false; } String tokenKey = getTokenKey(request); Object tokenObj = session.getAttribute(tokenKey); if(tokenObj==null) { rethrow("服務端不存在當前token,請重新請求表單"); } String serverToken = tokenObj.toString(); session.removeAttribute(tokenKey); System.out.println("remove server token:" + serverToken); return clientToken.equals(serverToken); } @Override public String getTokenKey(HttpServletRequest request) { return getTokenField(); } @Override public void saveToken(HttpServletRequest request) { HttpSession session = request.getSession(); String tokenKey = getTokenKey(request); Object tokenObj = session.getAttribute(tokenKey); String token; if (tokenObj == null) { token = MakeToken.getInstance().getToken(); // 服務端保存token session.setAttribute(tokenKey, token); } else { token = tokenObj.toString(); } System.out.println("current token:" + token); // 寫入cookie Cookie cookie = new Cookie(getTokenField(), token); response.addCookie(cookie); } private void rethrow(String message) { RuntimeException e = new RuntimeException(message); throw e; } @Override public String getClientToken(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies == null) { rethrow("沒有讀取到客戶端的cookie"); return null; } for (Cookie cookie : cookies) { if (cookie.getName().equals(getTokenKey(request))) { String cookieValue = cookie.getValue(); return cookieValue; } } rethrow("客戶端cookie中沒有存儲token"); return null; } }View Codehttps://github.com/monkeyming/AvoidDuplicateSubmission