防止表单重复提交
在Web开发中表单的重复提交是很严重的问题,重复提交成功会产生垃圾数据消耗不必要的资源,更严重的是如果遇到恶意刷库的情况垃圾数据更是数不胜数。在正常使用过程中产生重复提交的情况也有多重情况:鼠标连击、回退提交、刷新提交、网络延迟用户重复提交等。
防止重复提交的方法分两大类就是客户端、服务端(这是废话了)。客户端主要是用js对按钮的限制,一次点击后屏蔽按钮或者是直接跳转等待页面,服务端思路为客户端加token进行验证。客户端就不做详细介绍,主要介绍服务端的控制。
1、客户端存储
就是在客户端不同的地方存储两个token,在服务端进行校验。在Form表单中存储一个token利用隐藏域,在Cookie中存储一个(也可以都放到form表单中两个不同的隐藏域)。档form表单提交的时候,对这两个token进行验证,相同则允许提交否则阻止提交。
优点:
不占用服务器资源
实施起来简单,易上手
缺点:
容易伪造(防君子不防小人)
占用网络资源(或许不是那么明显)
详细介绍一下客户端分布存储在Form表单中和Cookie中的情况。
客户端的实现如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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(); } } }
2、双向存储
客户端和服务端的token各自独立存储,客户端存储在Cookie或者Form的隐藏域(放在Form隐藏域中的时候,需要每个表单)中,服务端存储在Session(单机系统中可以使用)或者其他缓存系统(分布式系统可以使用)中。
优点:
安全性高(几乎是无法伪造的)
网络资源相对于前者有所减少
缺点:
整个系统实施起来较第一种方法的时候复杂度增加
详细介绍一下服务端存储在session中客户端存储在Cookie中
SessionTokenProcesser实现如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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; } }
整个示例源码:
https://github.com/monkeyming/AvoidDuplicateSubmission
参考:
http://blog.csdn.net/h183288132/article/details/50184199
http://www.cnblogs.com/monkeyming/p/5599061.html