在上一篇中我们介绍了如果使用Session来做一个简单的用户登录案例,在本篇中我们继续使用Session技术来做一个防止表单重复提交的案例。

  这是一个很重要的知识点,在很多框架中都有防止表单重复提交的这个概念。表单重复提交,这个概念已经在字面意义上很明确的说明了,现实生活中会有各种重复提交情况的发生,比如当用户点击了提交按钮之后,由于网速的原因,页面没有及时跳转到相应的页面,导致用户以为自己没有提交,结果又多点了几次;又或者是在表单提交后页面跳转到Servlet处理表单数据时进行了多次刷新,都会导致服务器收到多次表单请求。

  要想阻止表单重复提交,需要在客户端和服务器端同时阻止。在客户端通过JavaScript,在服务器端使用程序。在客户端阻止可以减小服务器的压力,并且能提升用户的体验效果;在服务器端阻止主要防止使用浏览器另写表单而发送给服务器。

========================在客户端阻止表单重复提交========================

  那么先来解决如何在前台客户端防止表单重复提交。

  在客户端解决表单重复提交可以有两种方法:

  第一种,使用<form>表单标签的onsubmit事件方法。当onsubmit为“true”时代表已经提交了表单,这时再点击提交浏览器也不会响应,为此我需要使用JavaScript来编写一个判断表单是否已经提交的标志代码:

     <script type="text/javascript">
var isCommitted = false;
function doSubmit(){
/*如果表单提交了,就会触发该函数 */
5       /*如果表单未提交,则置isCommitted为true,否则置为false(防止再次提交) */
if(!isCommitted) {
isCommitted = true;
return true;
}
else{
return false;
}
}
</script>
<form action="/PreventResubmit/servlet/FormHandler" method="post" onsubmit="return doSubmit()" >
用户名<input type="text" name="username" /> <br>
   <input type="submit" value="提交" />
</form>

  另一种方法是将在表单页面点击提交按钮将该按钮失效,在<input type=”submit”>标签中有“disabled”属性,当设置了这个属性之后按钮会失效无法再点击,当然为了获取这个标签的节点,最好能为这个标签设一个id:

    <script type="text/javascript">
    function doSubmit(){
var submitNode = document.getElementById("submitIn");
submitNode.disabled = "disable";
return true ;
  }
</script>
<form action="/PreventResubmit/servlet/FormHandler" method="post" onsubmit="return doSubmit()" >
用户名<input type="text" name="username" /> <br>
<input type="submit" value="提交" id="submitIn"/>
</form>

  但无论上面哪种客户端阻止表单重复提交方式,都无法防止在提交之后的多次刷新依然会重复提交。

  所以必须还要依靠服务器端来阻止表单重复提交。而在服务器端防止,需要在程序(JSP或Servlet)中而不是HTML页面中写表单,然后在开始产生一个随机数,也送到客户端,当用户在客户端提交表单时会带着这个随机数和服务器端的随机数进行比较匹配,当随机数符合了,服务器立马就把自己的随机数删除,以后客户端再提交表单,即使带着随机数过来,服务器也没有随机数和他进行匹配了,这时候再次提交的表单都会被拒绝。

  那么我们要将随机数写在哪里呢,记得在HTML中针对<form>表单下有一个标签专门使用户看不到的数据会在表单提交时一起带给服务器,这个标签就是<input type=”hidden”>隐藏标签,我们只需要将随机数写在这里面即可。

  其次针对随机数的创建,这个随机数我们先采用一个类中实现方法来创建,至于原因在后面会说到,下面是通过Servlet来创建表单的代码(以后我就用JSP了!!!):

 public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException { response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter(); String token = TokenProcessor.getInstance().createToken(); writer.print("<form action='/PreventResubmit/servlet/FormHandler' method='POST'>");
writer.print("<input type='hidden' name='token' value='"+token+"' />");
writer.print("用户名:<input type='text' name='username' /> <br/>");
writer.print("<input type='submit' value='提交' />"); request.getSession().setAttribute("token",token); //用于表单提交后跳转的Servlet取出该属性来验证表单是否重复提交
}

  在上面代码中,主要有三行是比较重要的代码,分别用带颜色的标记标出了。第一次红行代码是根据一个单例模式类的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》。

综上我们产生随机数的最终代码为:

 class TokenProcessor {  //采用单例设计模式
private TokenProcessor(){}
private static final TokenProcessor tp = new TokenProcessor();
public static TokenProcessor getInstance() {
return tp;
} public String createToken(){
String token = "" + System.currentTimeMillis() + new Random().nextInt(99999999);
try {
MessageDigest md = MessageDigest.getInstance("md5");
byte[] md5 = md.digest(token.getBytes()); //128位,即16字节
//从digest方法返回的是字节数组,我们需要将字节数组转换回字符串
//但又不能将字节数组通过常见码表转换,因为字节数组中有负值元素,而码表并没有对应的字符。
//因此这里采用 base64 编码
BASE64Encoder be = new BASE64Encoder();
String newToken = be.encode(md5);
return newToken; } catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}

记住BASE64Encoder没有相关官方API文档,需自行上网查阅。

  到这里我们已经通过Servlet将表单页面完成,并使表单配有我们“专门提供”的随机数,为了这个随机数只能配对一次,也将其存进了Session中,我们不妨先使用浏览器来访问这个Servlet,并查看一下源文件:

  

可以看到我们通过系统时间和Random对象转换后的随机数非常像某些网上的验证码有没有!!至此,我们在表单页面上得工作已经完成。

  接下来,我们要另起一个Servlet来处理提交的表单了。

  处理表单提交的Servlet可以先判断表单中的“token”标识是否有效。这主要基于三种情况需要验证:

  1,这个提交的表单的请求中是否有“token”这个属性,如果没有,说明这个请求不是在表单中提交,可能是通过另外创建的表单提交,因此服务器应该置其为无效。

  2,服务器端Session中的“token”属性是否还在,如果Session中已经不存在这个属性了,说明之前表单已经提交过了,只有提交过才会将服务器Session中的该属性移除,因此服务器对于这次表单的提交应置为无效。

  3,客户端提交表单发来的请求中的“token”属性值是否与服务器端Session中的“token”属性值相同,主要用于表单首次提交的验证,也是我们整篇文章谈论下来为什么要为表单设置独一的随机数的原因。

  只有通过以上三种情况的验证,才说明这个表单是首次提交,并且是配对服务器端的验证的,这时候我们需要立马将服务器端Session中的“token”属性移除,这样我们就可以彻底杜绝表单重复提交了:

 public class FormHandler extends HttpServlet {

     public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException { //首先对“token”进行验证
boolean isSubmit = isTokenState(request); //isTokenState函数在后该函数之后 if(!isSubmit) {
System.out.println("该表单先前已经被提交,请勿重复提交,谢谢");
return ;
}
   }
    //isTokenState()函数的声明如下:
   private boolean isTokenState(HttpServletRequest request) { String client_token = request.getParameter("token");
String server_token = (String) request.getSession().getAttribute("token");
if(client_token == null ) { //防止客户端没有要校验的随机数
return false;
} if(server_token == null ){ //客户端已经提交过了,因此服务端不再有token标识,防止重复提交
return false;
} if(!client_token.equals(server_token)){ //防止其他含有token标识的Servlet来访问处理表单的Servlet
return false;
} return true; //如果以上三种验证均通过,则说明该token是有效地,且表单为首次提交
}
}

当首次提交表单之后,无论后面如何再次刷新,处理表单的Servlet都不会再处理表单的提交了。

Servlet的学习之Session(5)的更多相关文章

  1. Servlet的学习之Session(3)

    在上一篇<Servlet的学习之Session(2)>我们知道了Session能实现一个会话过程中保存数据或者多个会话中实现同一个Session的关键因素就是Cookie,只是Cookie ...

  2. Servlet的学习之Session(2)

    在上一篇中我们学习了Session对象默认在一个会话过程中,由服务器创建,能保存在这个会话过程中用户访问多个web资源时产生的需要保存的数据,并在访问服务器中其他web资源时可以将这些数据从Sessi ...

  3. Servlet的学习之Session(1)

    在学习完了Servlet中的Cookie技术后,我们再来学习另一个能保存会话数据的技术——Session. Session是服务器端技术,利用这个技术,服务器在运行时可以为每一个用户的浏览器创建一个其 ...

  4. Servlet的学习之Session(4)

    在本篇中,我们来使用Session完成一个用户登录的案例,前提声明:这个案例主要用于学习Session技术,是属于比较简单的类型,以后会采用MVC模式来开发登录,那就会比较复杂. 现在大多数网站都提供 ...

  5. JavaWeb之Servlet:Cookie 和 Session

    会话 现实生活中我们会用手机跟对方对话,拿起手机,拨号,然后对面接听,跟着互相通话,最后会话结束. 这个过程也可以用我们的B/S模式来描述: 打开浏览器—>输入地址->发出请求->服 ...

  6. Servlet的学习之Cookie

    从本篇开始学习Servlet技术中的Cookie专题. 首先来了解什么是“会话”.会话是web技术中的一个术语,可以简单的理解为:用户打开一个浏览器,点击多个超链接,访问服务器多个web资源,然后关闭 ...

  7. Servlet的学习之Filter过滤器技术(1)

    本篇将讲诉Servlet中一项非常重要的技术,Filter过滤器技术.通过过滤器,可以对来自客户端的请求进行拦截,进行预处理或者对最终响应给客户端的数据进行处理后再输出. 要想使用Filter过滤器, ...

  8. Servlet的学习(四)

    在本篇的Servlet的学习中,主要来学习由使用MyEclipse来开发Servlet的一些小细节. 细节一:在web.xml中可以对同一个Servlet配置多个对外访问路径,并如果在web.xml中 ...

  9. Servlet的学习之Request请求对象(3)

    本篇接上一篇,将Servlet中的HttpServletRequest对象获取RequestDispatcher对象后能进行的[转发]forward功能和[包含]include功能介绍完. 首先来看R ...

随机推荐

  1. 怎样让jQuery和其它js库共存

    怎样让jQuery和其它js库共存 有时候需要同时使用jQuery和其它javascript,比如在joomla中默认的是motools,但很多人还是希 望能够使用jQuery,如果直接调用的话,由于 ...

  2. win8.1镜像制作

    使用自带的powerShell工具,以管理员身份运行,比如镜像的目标位置为F盘,那么用下面的命令即可, wbAdmin start backup -backupTarget:F: -include:C ...

  3. 循环调用修正sic86 2改后的(除了第一年有点诡异,其他年份可以正常修复)

    create or replace procedure rebuild_sic86_wyl(pi_aac001 in number, po_fhz out varchar2, po_msg out v ...

  4. CodeForces 225C Barcode DP

    也是一道dp ,想到了就会觉得很巧妙 矩阵中只有白块和黑块,要求repaint后满足下述条件: 每列一种颜色 根据输入范围x, y 要求条纹宽度在[x, y] 之间 数据范围: n, m, x and ...

  5. SQLite数据转换成sql server数据

    需要将SQLite数据库里的数据导入到SQL Server,在网上搜了好久,没有找到一个方便实用的方法. 经过本人的细心琢磨实验,终于让我给找到一好的方法:使用CSV文件作为介质来做转换.现在记录下来 ...

  6. POJ 3261 Milk Patterns(后缀数组+二分答案+离散化)

    题意:给定一个字符串,求至少出现k 次的最长重复子串,这k 个子串可以重叠. 分析:经典的后缀数组求解题:先二分答案,然后将后缀分成若干组.这里要判断的是有没有一个组的符合要求的后缀个数(height ...

  7. Actor::updateMassFromShapes

    unity报错Actor::updateMassFromShapes: Compute mesh inertia tensor failed for one of the actor's mesh s ...

  8. mysql的基础操作

    查看数据库 获取服务器上的数据库列表通常很有用.执行show databases;命令就可以搞定. mysql> show databases; 创建数据库 mysql> create d ...

  9. Oracle中查询各种对象的方法小结

    --查看当前库中的所有表select * from all_tables a where a.table_name='INFOCODE_P20081'--查看表结构select * from all_ ...

  10. Python之路day4

    坚持就是胜利.今天零下14度,从教室出来的路上真的很冷很冷,希望这个冬天自己不会白过,春暖花开的时候一定要给世界一个更好的自己. 原本以为day3的作业自己做得挺好的,没想到只得了B+.必须要加油了, ...