在上一篇中我们介绍了如果使用Session来做一个简单的用户登录案例,在本篇中我们继续使用Session技术来做一个防止表单重复提交的案例。
这是一个很重要的知识点,在很多框架中都有防止表单重复提交的这个概念。表单重复提交,这个概念已经在字面意义上很明确的说明了,现实生活中会有各种重复提交情况的发生,比如当用户点击了提交按钮之后,由于网速的原因,页面没有及时跳转到相应的页面,导致用户以为自己没有提交,结果又多点了几次;又或者是在表单提交后页面跳转到Servlet处理表单数据时进行了多次刷新,都会导致服务器收到多次表单请求。
要想阻止表单重复提交,需要在客户端和服务器端同时阻止。在客户端通过JavaScript,在服务器端使用程序。在客户端阻止可以减小服务器的压力,并且能提升用户的体验效果;在服务器端阻止主要防止使用浏览器另写表单而发送给服务器。
========================在客户端阻止表单重复提交========================
那么先来解决如何在前台客户端防止表单重复提交。
在客户端解决表单重复提交可以有两种方法:
第一种,使用<form>表单标签的onsubmit事件方法。当onsubmit为“true”时代表已经提交了表单,这时再点击提交浏览器也不会响应,为此我需要使用JavaScript来编写一个判断表单是否已经提交的标志代码:
1 <script type="text/javascript"> 2 var isCommitted = false; 3 function doSubmit(){ 4 /*如果表单提交了,就会触发该函数 */ 5 /*如果表单未提交,则置isCommitted为true,否则置为false(防止再次提交) */ 6 if(!isCommitted) { 7 isCommitted = true; 8 return true; 9 } 10 else{ 11 return false; 12 } 13 } 14 </script> 15 <form action="/PreventResubmit/servlet/FormHandler" method="post" onsubmit="return doSubmit()" > 16 用户名<input type="text" name="username" /> <br> 17 <input type="submit" value="提交" /> 18 </form>
另一种方法是将在表单页面点击提交按钮将该按钮失效,在<input type=”submit”>标签中有“disabled”属性,当设置了这个属性之后按钮会失效无法再点击,当然为了获取这个标签的节点,最好能为这个标签设一个id:
1 <script type="text/javascript"> 2 function doSubmit(){ 3 var submitNode = document.getElementById("submitIn"); 4 submitNode.disabled = "disable"; 5 return true ; 6 } 7 </script> 8 <form action="/PreventResubmit/servlet/FormHandler" method="post" onsubmit="return doSubmit()" > 9 用户名<input type="text" name="username" /> <br> 10 <input type="submit" value="提交" id="submitIn"/> 11 </form>
但无论上面哪种客户端阻止表单重复提交方式,都无法防止在提交之后的多次刷新依然会重复提交。
所以必须还要依靠服务器端来阻止表单重复提交。而在服务器端防止,需要在程序(JSP或Servlet)中而不是HTML页面中写表单,然后在开始产生一个随机数,也送到客户端,当用户在客户端提交表单时会带着这个随机数和服务器端的随机数进行比较匹配,当随机数符合了,服务器立马就把自己的随机数删除,以后客户端再提交表单,即使带着随机数过来,服务器也没有随机数和他进行匹配了,这时候再次提交的表单都会被拒绝。
那么我们要将随机数写在哪里呢,记得在HTML中针对<form>表单下有一个标签专门使用户看不到的数据会在表单提交时一起带给服务器,这个标签就是<input type=”hidden”>隐藏标签,我们只需要将随机数写在这里面即可。
其次针对随机数的创建,这个随机数我们先采用一个类中实现方法来创建,至于原因在后面会说到,下面是通过Servlet来创建表单的代码(以后我就用JSP了!!!):
1 public void doGet(HttpServletRequest request, HttpServletResponse response) 2 throws ServletException, IOException { 3 4 response.setCharacterEncoding("UTF-8"); 5 response.setContentType("text/html;charset=utf-8"); 6 PrintWriter writer = response.getWriter(); 7 8 String token = TokenProcessor.getInstance().createToken(); 9 10 writer.print("<form action='/PreventResubmit/servlet/FormHandler' method='POST'>"); 11 writer.print("<input type='hidden' name='token' value='"+token+"' />"); 12 writer.print("用户名:<input type='text' name='username' /> <br/>"); 13 writer.print("<input type='submit' value='提交' />"); 14 15 request.getSession().setAttribute("token",token); //用于表单提交后跳转的Servlet取出该属性来验证表单是否重复提交 16 }
在上面代码中,主要有三行是比较重要的代码,分别用带颜色的标记标出了。第一次红行代码是根据一个单例模式类的createToken()方法产生一个“随机数”,这里命名为“token”,具体会在稍后说明。
在<input type=”hidden”>标签中将该标签的”value”属性值设为刚刚创建的”token”。
最后一次出现的红行代码是将“token”作为Session中的一个属性,以便于在处理表单的Servlet中取出并和首次提交表单中的“hidden”值进行比较,并做销毁以防表单再次提交。
那么我们该重点来说明下这个“随机数”的产生了。
我们将随机数以一个方法来产生,而这个方法是在一个单例模式的类中。这里你可能会问为什么要采用单例模式,这是因为正适合这个案例的情况。由于对应不同的用户有着不同的Session,那么我们应该尽可能的要使每个用户能得到不一致的随机数,而如果以一个对象来产生所有的随机数重复的概率不是比多个类一起来产生随机数的概率要小的多吗,因此我们采用一个对象来为所有的Session产生真正独一无二的验证。
那么如何产生随机数呢?
随机数的产生,我们可以以某一个时间毫秒值加上一个随机数来产生,例如:
String token = "" + System.currentTimeMillis() + new Random().nextInt(99999999);
但是这样随机数的产生会有一个问题,因为我们最后跟随的Random对象产生的随机数位数不一定一致,可能是45这样的两位数,也可能是2324335这样的多位数,为了能将所有的随机数都能保持一样的位数,于是我们采用MD5来取信息摘要(或称数据指纹)。
关于在Java中使用MD5请看我的另一篇博客《在Java中使用MD5和BASE64》。
当然在这里我们只想使用MD5来将我们产生的随机数转换成同等长度的新随机数。通过MessageDigest对象来调用方法获取某些数字组合的MD5码得到的都是128位,即16字节(就是字节数组中有16个元素,同时byte的范围为-128~127)。这样就满足了我们所有的随机数都具有相同的位数。我们现在只需要将字节数组转换为字符串即可。
由于通过MD5获取的字节数组中含有负值元素,因此我们不能通过常见的new String(byte[],”UTF-8”)或者new String(byte[],”GB2312”)等等这样的方式,因为负值在这些码表中没有对应的字符,因此会产生乱码。
这里我们使用base64编码来解决这个问题,关于在Java中使用Base64编码也请看这篇博客《在Java中使用MD5和BASE64》。
综上我们产生随机数的最终代码为:
1 class TokenProcessor { //采用单例设计模式 2 private TokenProcessor(){} 3 private static final TokenProcessor tp = new TokenProcessor(); 4 public static TokenProcessor getInstance() { 5 return tp; 6 } 7 8 public String createToken(){ 9 String token = "" + System.currentTimeMillis() + new Random().nextInt(99999999); 10 try { 11 MessageDigest md = MessageDigest.getInstance("md5"); 12 byte[] md5 = md.digest(token.getBytes()); //128位,即16字节 13 //从digest方法返回的是字节数组,我们需要将字节数组转换回字符串 14 //但又不能将字节数组通过常见码表转换,因为字节数组中有负值元素,而码表并没有对应的字符。 15 //因此这里采用 base64 编码 16 BASE64Encoder be = new BASE64Encoder(); 17 String newToken = be.encode(md5); 18 return newToken; 19 20 } catch (NoSuchAlgorithmException e) { 21 throw new RuntimeException(e); 22 } 23 } 24 }
记住BASE64Encoder没有相关官方API文档,需自行上网查阅。
到这里我们已经通过Servlet将表单页面完成,并使表单配有我们“专门提供”的随机数,为了这个随机数只能配对一次,也将其存进了Session中,我们不妨先使用浏览器来访问这个Servlet,并查看一下源文件:
可以看到我们通过系统时间和Random对象转换后的随机数非常像某些网上的验证码有没有!!至此,我们在表单页面上得工作已经完成。
接下来,我们要另起一个Servlet来处理提交的表单了。
处理表单提交的Servlet可以先判断表单中的“token”标识是否有效。这主要基于三种情况需要验证:
1,这个提交的表单的请求中是否有“token”这个属性,如果没有,说明这个请求不是在表单中提交,可能是通过另外创建的表单提交,因此服务器应该置其为无效。
2,服务器端Session中的“token”属性是否还在,如果Session中已经不存在这个属性了,说明之前表单已经提交过了,只有提交过才会将服务器Session中的该属性移除,因此服务器对于这次表单的提交应置为无效。
3,客户端提交表单发来的请求中的“token”属性值是否与服务器端Session中的“token”属性值相同,主要用于表单首次提交的验证,也是我们整篇文章谈论下来为什么要为表单设置独一的随机数的原因。
只有通过以上三种情况的验证,才说明这个表单是首次提交,并且是配对服务器端的验证的,这时候我们需要立马将服务器端Session中的“token”属性移除,这样我们就可以彻底杜绝表单重复提交了:
1 public class FormHandler extends HttpServlet { 2 3 public void doPost(HttpServletRequest request, HttpServletResponse response) 4 throws ServletException, IOException { 5 6 //首先对“token”进行验证 7 boolean isSubmit = isTokenState(request); //isTokenState函数在后该函数之后 8 9 if(!isSubmit) { 10 System.out.println("该表单先前已经被提交,请勿重复提交,谢谢"); 11 return ; 12 } 13 } 14 //isTokenState()函数的声明如下: 15 private boolean isTokenState(HttpServletRequest request) { 16 17 String client_token = request.getParameter("token"); 18 String server_token = (String) request.getSession().getAttribute("token"); 19 if(client_token == null ) { //防止客户端没有要校验的随机数 20 return false; 21 } 22 23 if(server_token == null ){ //客户端已经提交过了,因此服务端不再有token标识,防止重复提交 24 return false; 25 } 26 27 if(!client_token.equals(server_token)){ //防止其他含有token标识的Servlet来访问处理表单的Servlet 28 return false; 29 } 30 31 return true; //如果以上三种验证均通过,则说明该token是有效地,且表单为首次提交 32 } 33 }
当首次提交表单之后,无论后面如何再次刷新,处理表单的Servlet都不会再处理表单的提交了。