zoukankan      html  css  js  c++  java
  • 利用session防止表单重复提交

    转自:http://www.cnblogs.com/xdp-gacl/p/3859416.html

    利用Session防止表单重复提交

      对于【场景二】和【场景三】导致表单重复提交的问题,既然客户端无法解决,那么就在服务器端解决,在服务器端解决就需要用到session了。

      具体的做法:在服务器端生成一个唯一的随机标识号,专业术语称为Token(令牌),同时在当前用户的Session域中保存这个Token。然后将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的时候连同这个Token一起提交到服务器端,然后在服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单。如果相同则处理表单提交,处理完后清除当前用户的Session域中存储的标识号。
      在下列情况下,服务器程序将拒绝处理用户提交的表单请求:

    1. 存储Session域中的Token(令牌)与表单提交的Token(令牌)不同。
    2. 当前用户的Session中不存在Token(令牌)
    3. 用户提交的表单数据中没有Token(令牌)

    看具体的范例:

      1.创建FormServlet,用于生成Token(令牌)和跳转到form.jsp页面

    复制代码
     1 package xdp.gacl.session;
     2 
     3 import java.io.IOException;
     4 import javax.servlet.ServletException;
     5 import javax.servlet.http.HttpServlet;
     6 import javax.servlet.http.HttpServletRequest;
     7 import javax.servlet.http.HttpServletResponse;
     8 
     9 public class FormServlet extends HttpServlet {
    10     private static final long serialVersionUID = -884689940866074733L;
    11 
    12     public void doGet(HttpServletRequest request, HttpServletResponse response)
    13             throws ServletException, IOException {
    14 
    15         String token = TokenProccessor.getInstance().makeToken();//创建令牌
    16         System.out.println("在FormServlet中生成的token:"+token);
    17         request.getSession().setAttribute("token", token);  //在服务器使用session保存token(令牌)
    18         request.getRequestDispatcher("/form.jsp").forward(request, response);//跳转到form.jsp页面
    19     }
    20 
    21     public void doPost(HttpServletRequest request, HttpServletResponse response)
    22             throws ServletException, IOException {
    23         doGet(request, response);
    24     }
    25 
    26 }
    复制代码

      2.在form.jsp中使用隐藏域来存储Token(令牌)

    复制代码
     1 <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
     2 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
     3 <html>
     4 <head>
     5 <title>form表单</title>
     6 </head>
     7 
     8 <body>
     9     <form action="${pageContext.request.contextPath}/servlet/DoFormServlet" method="post">
    10         <%--使用隐藏域存储生成的token--%>
    11         <%--
    12             <input type="hidden" name="token" value="<%=session.getAttribute("token") %>">
    13         --%>
    14         <%--使用EL表达式取出存储在session中的token--%>
    15         <input type="hidden" name="token" value="${token}"/> 
    16         用户名:<input type="text" name="username"> 
    17         <input type="submit" value="提交">
    18     </form>
    19 </body>
    20 </html>
    复制代码

      3.DoFormServlet处理表单提交

    复制代码
     1 package xdp.gacl.session;
     2 
     3 import java.io.IOException;
     4 import javax.servlet.ServletException;
     5 import javax.servlet.http.HttpServlet;
     6 import javax.servlet.http.HttpServletRequest;
     7 import javax.servlet.http.HttpServletResponse;
     8 
     9 public class DoFormServlet extends HttpServlet {
    10 
    11     public void doGet(HttpServletRequest request, HttpServletResponse response)
    12                 throws ServletException, IOException {
    13 
    14             boolean b = isRepeatSubmit(request);//判断用户是否是重复提交
    15             if(b==true){
    16                 System.out.println("请不要重复提交");
    17                 return;
    18             }
    19             request.getSession().removeAttribute("token");//移除session中的token
    20             System.out.println("处理用户提交请求!!");
    21         }
    22         
    23         /**
    24          * 判断客户端提交上来的令牌和服务器端生成的令牌是否一致
    25          * @param request
    26          * @return 
    27          *         true 用户重复提交了表单 
    28          *         false 用户没有重复提交表单
    29          */
    30         private boolean isRepeatSubmit(HttpServletRequest request) {
    31             String client_token = request.getParameter("token");
    32             //1、如果用户提交的表单数据中没有token,则用户是重复提交了表单
    33             if(client_token==null){
    34                 return true;
    35             }
    36             //取出存储在Session中的token
    37             String server_token = (String) request.getSession().getAttribute("token");
    38             //2、如果当前用户的Session中不存在Token(令牌),则用户是重复提交了表单
    39             if(server_token==null){
    40                 return true;
    41             }
    42             //3、存储在Session中的Token(令牌)与表单提交的Token(令牌)不同,则用户是重复提交了表单
    43             if(!client_token.equals(server_token)){
    44                 return true;
    45             }
    46             
    47             return false;
    48         }
    49 
    50     public void doPost(HttpServletRequest request, HttpServletResponse response)
    51             throws ServletException, IOException {
    52         doGet(request, response);
    53     }
    54 
    55 }
    复制代码

      生成Token的工具类TokenProccessor

    复制代码
     1 package xdp.gacl.session;
     2 
     3 import java.security.MessageDigest;
     4 import java.security.NoSuchAlgorithmException;
     5 import java.util.Random;
     6 import sun.misc.BASE64Encoder;
     7 
     8 public class TokenProccessor {
     9 
    10     /*
    11      *单例设计模式(保证类的对象在内存中只有一个)
    12      *1、把类的构造函数私有
    13      *2、自己创建一个类的对象
    14      *3、对外提供一个公共的方法,返回类的对象
    15      */
    16     private TokenProccessor(){}
    17     
    18     private static final TokenProccessor instance = new TokenProccessor();
    19     
    20     /**
    21      * 返回类的对象
    22      * @return
    23      */
    24     public static TokenProccessor getInstance(){
    25         return instance;
    26     }
    27     
    28     /**
    29      * 生成Token
    30      * Token:Nv6RRuGEVvmGjB+jimI/gw==
    31      * @return
    32      */
    33     public String makeToken(){  //checkException
    34         //  7346734837483  834u938493493849384  43434384
    35         String token = (System.currentTimeMillis() + new Random().nextInt(999999999)) + "";
    36         //数据指纹   128位长   16个字节  md5
    37         try {
    38             MessageDigest md = MessageDigest.getInstance("md5");
    39             byte md5[] =  md.digest(token.getBytes());
    40             //base64编码--任意二进制编码明文字符   adfsdfsdfsf
    41             BASE64Encoder encoder = new BASE64Encoder();
    42             return encoder.encode(md5);
    43         } catch (NoSuchAlgorithmException e) {
    44             throw new RuntimeException(e);
    45         }
    46     }
    47 }
    复制代码

    利用Session防止表单重复提交

      对于【场景二】和【场景三】导致表单重复提交的问题,既然客户端无法解决,那么就在服务器端解决,在服务器端解决就需要用到session了。

      具体的做法:在服务器端生成一个唯一的随机标识号,专业术语称为Token(令牌),同时在当前用户的Session域中保存这个Token。然后将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的时候连同这个Token一起提交到服务器端,然后在服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单。如果相同则处理表单提交,处理完后清除当前用户的Session域中存储的标识号。
      在下列情况下,服务器程序将拒绝处理用户提交的表单请求:

    1. 存储Session域中的Token(令牌)与表单提交的Token(令牌)不同。
    2. 当前用户的Session中不存在Token(令牌)
    3. 用户提交的表单数据中没有Token(令牌)

    看具体的范例:

      1.创建FormServlet,用于生成Token(令牌)和跳转到form.jsp页面

    复制代码
     1 package xdp.gacl.session;
     2 
     3 import java.io.IOException;
     4 import javax.servlet.ServletException;
     5 import javax.servlet.http.HttpServlet;
     6 import javax.servlet.http.HttpServletRequest;
     7 import javax.servlet.http.HttpServletResponse;
     8 
     9 public class FormServlet extends HttpServlet {
    10     private static final long serialVersionUID = -884689940866074733L;
    11 
    12     public void doGet(HttpServletRequest request, HttpServletResponse response)
    13             throws ServletException, IOException {
    14 
    15         String token = TokenProccessor.getInstance().makeToken();//创建令牌
    16         System.out.println("在FormServlet中生成的token:"+token);
    17         request.getSession().setAttribute("token", token);  //在服务器使用session保存token(令牌)
    18         request.getRequestDispatcher("/form.jsp").forward(request, response);//跳转到form.jsp页面
    19     }
    20 
    21     public void doPost(HttpServletRequest request, HttpServletResponse response)
    22             throws ServletException, IOException {
    23         doGet(request, response);
    24     }
    25 
    26 }
    复制代码

      2.在form.jsp中使用隐藏域来存储Token(令牌)

    复制代码
     1 <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
     2 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
     3 <html>
     4 <head>
     5 <title>form表单</title>
     6 </head>
     7 
     8 <body>
     9     <form action="${pageContext.request.contextPath}/servlet/DoFormServlet" method="post">
    10         <%--使用隐藏域存储生成的token--%>
    11         <%--
    12             <input type="hidden" name="token" value="<%=session.getAttribute("token") %>">
    13         --%>
    14         <%--使用EL表达式取出存储在session中的token--%>
    15         <input type="hidden" name="token" value="${token}"/> 
    16         用户名:<input type="text" name="username"> 
    17         <input type="submit" value="提交">
    18     </form>
    19 </body>
    20 </html>
    复制代码

      3.DoFormServlet处理表单提交

    复制代码
     1 package xdp.gacl.session;
     2 
     3 import java.io.IOException;
     4 import javax.servlet.ServletException;
     5 import javax.servlet.http.HttpServlet;
     6 import javax.servlet.http.HttpServletRequest;
     7 import javax.servlet.http.HttpServletResponse;
     8 
     9 public class DoFormServlet extends HttpServlet {
    10 
    11     public void doGet(HttpServletRequest request, HttpServletResponse response)
    12                 throws ServletException, IOException {
    13 
    14             boolean b = isRepeatSubmit(request);//判断用户是否是重复提交
    15             if(b==true){
    16                 System.out.println("请不要重复提交");
    17                 return;
    18             }
    19             request.getSession().removeAttribute("token");//移除session中的token
    20             System.out.println("处理用户提交请求!!");
    21         }
    22         
    23         /**
    24          * 判断客户端提交上来的令牌和服务器端生成的令牌是否一致
    25          * @param request
    26          * @return 
    27          *         true 用户重复提交了表单 
    28          *         false 用户没有重复提交表单
    29          */
    30         private boolean isRepeatSubmit(HttpServletRequest request) {
    31             String client_token = request.getParameter("token");
    32             //1、如果用户提交的表单数据中没有token,则用户是重复提交了表单
    33             if(client_token==null){
    34                 return true;
    35             }
    36             //取出存储在Session中的token
    37             String server_token = (String) request.getSession().getAttribute("token");
    38             //2、如果当前用户的Session中不存在Token(令牌),则用户是重复提交了表单
    39             if(server_token==null){
    40                 return true;
    41             }
    42             //3、存储在Session中的Token(令牌)与表单提交的Token(令牌)不同,则用户是重复提交了表单
    43             if(!client_token.equals(server_token)){
    44                 return true;
    45             }
    46             
    47             return false;
    48         }
    49 
    50     public void doPost(HttpServletRequest request, HttpServletResponse response)
    51             throws ServletException, IOException {
    52         doGet(request, response);
    53     }
    54 
    55 }
    复制代码

      生成Token的工具类TokenProccessor

    复制代码
     1 package xdp.gacl.session;
     2 
     3 import java.security.MessageDigest;
     4 import java.security.NoSuchAlgorithmException;
     5 import java.util.Random;
     6 import sun.misc.BASE64Encoder;
     7 
     8 public class TokenProccessor {
     9 
    10     /*
    11      *单例设计模式(保证类的对象在内存中只有一个)
    12      *1、把类的构造函数私有
    13      *2、自己创建一个类的对象
    14      *3、对外提供一个公共的方法,返回类的对象
    15      */
    16     private TokenProccessor(){}
    17     
    18     private static final TokenProccessor instance = new TokenProccessor();
    19     
    20     /**
    21      * 返回类的对象
    22      * @return
    23      */
    24     public static TokenProccessor getInstance(){
    25         return instance;
    26     }
    27     
    28     /**
    29      * 生成Token
    30      * Token:Nv6RRuGEVvmGjB+jimI/gw==
    31      * @return
    32      */
    33     public String makeToken(){  //checkException
    34         //  7346734837483  834u938493493849384  43434384
    35         String token = (System.currentTimeMillis() + new Random().nextInt(999999999)) + "";
    36         //数据指纹   128位长   16个字节  md5
    37         try {
    38             MessageDigest md = MessageDigest.getInstance("md5");
    39             byte md5[] =  md.digest(token.getBytes());
    40             //base64编码--任意二进制编码明文字符   adfsdfsdfsf
    41             BASE64Encoder encoder = new BASE64Encoder();
    42             return encoder.encode(md5);
    43         } catch (NoSuchAlgorithmException e) {
    44             throw new RuntimeException(e);
    45         }
    46     }
    47 }
    复制代码

    补充:base64

    Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。可查看RFC2045~RFC2049,上面有MIME的详细规范。
    Base64编码是从二进制到字符的过程,可用于在HTTP环境下传递较长的标识信息。例如,在Java Persistence系统Hibernate中,就采用了Base64来将一个较长的唯一标识符(一般为128-bit的UUID)编码为一个字符串,用作HTTP表单和HTTP GET URL中的参数。在其他应用程序中,也常常需要把二进制数据编码为适合放在URL(包括隐藏表单域)中的形式。此时,采用Base64编码具有不可读性,需要解码后才能阅读。

    如:

    转码过程例子:
    3*8=4*6
    内存1个字节占8位
    转前: s 1 3
    先转成ascii:对应 115 49 51
    2进制: 01110011 00110001 00110011
    (将十进制转成二进制方法:除2,写出每一步的余数,将余数倒过来,高位补0(凑够8位))
    6个一组(4组) 011100110011000100110011
    然后才有后面的 011100 110011 000100 110011
    然后计算机是8位8位的存数 6不够,自动就补两个高位0了
    所有有了 高位补0
    科学计算器输入 00011100 00110011 00000100 00110011
    得到 28 51 4 51
    查对下照表 c z E z
    先以“迅雷下载”为例: 很多下载类网站都提供“迅雷下载”的链接,其地址通常是加密的迅雷专用下载地址。
    其实迅雷的“专用地址”也是用Base64"加密"的,其过程如下:
    一、在地址的前后分别添加AA和ZZ
    二、对新的字符串进行Base64编码
    另: Flashget的与迅雷类似,只不过在第一步时加的“料”不同罢了,Flashget在地址前后加的“料”是[FLASHGET]
    而QQ旋风的干脆不加料,直接就对地址进行Base64编码了
     

    应用

    Base64编码可用于在HTTP环境下传递较长的标识信息。例如,在Java Persistence系统Hibernate中,就采用了Base64来将一个较长的唯一标识符(一般为128-bit的UUID)编码为一个字符串,用作HTTP表单和HTTP GET URL中的参数。在其他应用程序中,也常常需要把二进制数据编码为适合放在URL(包括隐藏表单域)中的形式。此时,采用Base64编码不仅比较简短,同时也具有不可读性,即所编码的数据不会被人用肉眼所直接看到。

    然而,标准的Base64并不适合直接放在URL里传输,因为URL编码器会把标准Base64中的“/”和“+”字符变为形如“%XX”的形式,而这些“%”号在存入数据库时还需要再进行转换,因为ANSI SQL中已将“%”号用作通配符。
    为解决此问题,可采用一种用于URL的改进Base64编码,它不仅在末尾去掉填充的'='号,并将标准Base64中的“+”和“/”分别改成了“-”和“_”,这样就免去了在URL编解码和数据库存储时所要作的转换,避免了编码信息长度在此过程中的增加,并统一了数据库、表单等处对象标识符的格式。

    另有一种用于正则表达式的改进Base64变种,它将“+”和“/”改成了“!”和“-”,因为“+”,“*”以及前面在IRCu中用到的“[”和“]”在正则表达式中都可能具有特殊含义。
    此外还有一些变种,它们将“+/”改为“_-”或“._”(用作编程语言中的标识符名称)或“.-”(用于XML中的Nmtoken)甚至“_:”(用于XML中的Name)。
    Base64要求把每三个8Bit的字节转换为四个6Bit的字节(3*8 = 4*6 = 24),然后把6Bit再添两位高位0,组成四个8Bit的字节,也就是说,转换后的字符串理论上将要比原来的长1/3。

    规则

    关于这个编码的规则:
    ①.把3个字符变成4个字符。
    ②每76个字符加一个换行符。
    ③.最后的结束符也要处理。
     

    例子:

    转换前 11111111, 11111111, 11111111 (二进制)
    转换后 00111111, 00111111, 00111111, 00111111 (二进制)
    上面的三个字节是原文,下面的四个字节是转换后的Base64编码,其前两位均为0。
    转换后,我们用一个码表来得到我们想要的字符串(也就是最终的Base64编码),这个表是这样的:(摘自RFC2045)
     
    转换表
    Table 1: The Base64 Alphabet

    索引
    对应字符
    索引
    对应字符
    索引
    对应字符
    索引
    对应字符
    0
    A
    17
    R
    34
    i
    51
    z
    1
    B
    18
    S
    35
    j
    52
    0
    2
    C
    19
    T
    36
    k
    53
    1
    3
    D
    20
    U
    37
    l
    54
    2
    4
    E
    21
    V
    38
    m
    55
    3
    5
    F
    22
    W
    39
    n
    56
    4
    6
    G
    23
    X
    40
    o
    57
    5
    7
    H
    24
    Y
    41
    p
    58
    6
    8
    I
    25
    Z
    42
    q
    59
    7
    9
    J
    26
    a
    43
    r
    60
    8
    10
    K
    27
    b
    44
    s
    61
    9
    11
    L
    28
    c
    45
    t
    62
    +
    12
    M
    29
    d
    46
    u
    63
    /
    13
    N
    30
    e
    47
    v
       
    14
    O
    31
    f
    48
    w
       
    15
    P
    32
    g
    49
    x
       
    16
    Q
    33
    h
    50
    y
     

    留点什么

  • 相关阅读:
    Android Gradle Plugin指南(五)——Build Variants(构建变种版本号)
    文件内容操作篇clearerr fclose fdopen feof fflush fgetc fgets fileno fopen fputc fputs fread freopen fseek ftell fwrite getc getchar gets
    文件操作篇 close creat dup dup2 fcntl flock fsync lseek mkstemp open read sync write
    嵌入式linux应用程序调试方法
    version control system:git/hg/subversion/cvs/clearcase/vss。software configruation management。代码集成CI:Cruisecontrol/hudson/buildbot
    最值得你所关注的10个C语言开源项目
    如何记录linux终端下的操作日志
    CentOS 5.5 虚拟机安装 VirtualBox 客户端增强功能
    sizeof, strlen区别
    C/C++嵌入式开发面试题
  • 原文地址:https://www.cnblogs.com/yangyongjie/p/7567687.html
Copyright © 2011-2022 走看看