• 表单重复提交的解决方案(防止模拟Http请求CSRF)


    原创itcats_cn 最后发布于2018-09-02 23:52:29 阅读数 2196 收藏
    展开
    引发表单重复提交的一些场景:
    网络延时
     在平时开发中,如果网速比较慢的情况下,用户提交表单后,发现服务器半天都没有响应,那么用户可能会以为是自己没有提交表单,就会再点击提交按钮重复提交表单,我们在开发中必须防止表单重复提交。

    重新刷新
    表单提交后用户点击【刷新】按钮导致表单重复提交

    点击浏览器的【后退】按钮回退到表单页面后进行再次提交
    用户提交表单后,点击浏览器的【后退】按钮回退到表单页面后进行再次提交

    场景模拟
    创建一个form.jsp页面

    <%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <title>Form表单</title>
    </head>

    <body>
    <form action="${pageContext.request.contextPath}/DoFormServlet" method="post">
    用户名:<input type="text" name="userName">
    <input type="submit" value="提交" id="submit">
    </form>
    </body>
    </html>
     

    @WebServlet("/DoFormServlet")
    public class DoFormServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    req.setCharacterEncoding("UTF-8");
    String userName = req.getParameter("userName");
    try {
    //模拟网络延迟
    Thread.sleep(300);
    } catch (Exception e) {
    }
    System.out.println("往数据库插入数据...."+userName);
    resp.getWriter().write("success");
    }

    }
     

    发现在点击多次"提交按钮"、刷新页面、回退到表单时产生了多次提交的情况。

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

    存储Session域中的Token(令牌)与表单提交的Token(令牌)不同。
    当前用户的Session中不存在Token(令牌)。
    用户提交的表单数据中没有Token(令牌)。 
    注:有条件则使用redis代替session。

    实现流程:

    ToFormServlet(生成token放在session域)--->转发到form.jsp,隐藏域获取session中的token--->提交表单--->DoFormServlet(判断表单隐藏域的token和session中的token)

    @WebServlet("/ToFormServlet")
    public class ToFormServlet extends HttpServlet{
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //1.生成token
    String token = UUID.randomUUID().toString();
    //2.将token放入session域中
    req.getSession().setAttribute("formToken", token);
    //3.转发到form.jsp
    req.getRequestDispatcher("form.jsp").forward(req, resp);
    }
    }
     

    <form action="${pageContext.request.contextPath}/DoFormServlet" method="post">
    用户名:<input type="text" name="userName">
    <!-- 携带formToken 放在隐藏域,用于在DoFormServlet获取token-->
    <input type="hidden" name="formToken" value="${formToken}">
    <input type="submit" value="提交" id="submit">
    </form>
     

    @WebServlet("/DoFormServlet")
    public class DoFormServlet extends HttpServlet {

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    req.setCharacterEncoding("UTF-8");
    //返回false,提交过了
    if(!isFlag(req)){
    resp.getWriter().print("you have already reported...");
    return;
    }
    String userName = req.getParameter("userName");
    try {
    Thread.sleep(300);
    } catch (Exception e) {
    }
    System.out.println("往数据库插入数据...." + userName);
    resp.getWriter().write("success");
    }

    /**
    * 返回flase的情况 1. 存储Session域中的Token(令牌)与表单提交的Token(令牌)不同。 2.
    * 当前用户的Session中不存在Token(令牌)。 3. 用户提交的表单数据中没有Token(令牌)。
    * 当token与seesion中的formToken一致,返回true
    *
    * @return
    */
    public boolean isFlag(HttpServletRequest request) {
    // 获取form.jsp传递过来的formToken
    String token = request.getParameter("formToken");
    if (StringUtils.isBlank(token)) {
    // jsp页面中没有传递token
    System.out.println("token没有传递过来");
    return false;
    }
    // 获取session中的formToken
    String formToken = (String) request.getSession().getAttribute("formToken");
    if (StringUtils.isBlank(formToken)) {
    System.out.println("session中的token已经被清空,请不要重复提交表单");
    }
    // 比较formToken和token是否一致,防止伪造token
    if (!token.equals(formToken)) {
    System.out.println("请不要伪造token...表单中的token:" + token + " session中的formToken" + formToken);
    return false;
    }
    // 删除session中的token
    request.getSession().removeAttribute("formToken");
    return true;
    }

    }
    改进:不要用session进行存储,把UUID生成的token存到redis中。使用redis进行存取,效率更高,稳定性高,资源占用更小。

    如何防止模拟http请求?(CSRF)
    Cross-site request forgery   跨站请求伪造

    上面已经回答了这个问题,使用令牌,令牌具有唯一性,在完成一次请求后清空令牌,所以一个令牌只能有一次请求。

    可是有些时候token被知道了,还是能模拟请求,那么就需要使用验证码。

    所以,想要防止模拟http请求   token+验证码  


    ————————————————
    版权声明:本文为CSDN博主「itcats_cn」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/itcats_cn/article/details/82319690

  • 相关阅读:
    使用jmeter进行性能测试-Jmeter教程及技巧汇总 (转)
    Linux防火墙(Iptables)的开启与关闭
    解决Unable to load R3 module ...VBoxDD.dll (VBoxDD):GetLastError=1790
    Linux如何修改文件/文件夹内所有文件的权限
    php抽象类的简单应用
    php接口和多态的概念以及简单应用
    关于php中数据访问的几点补充
    php中重写和final关键字的使用
    php中static静态关键字的使用
    php对象引用和析构函数的关系
  • 原文地址:https://www.cnblogs.com/fengff/p/12576716.html
Copyright © 2020-2023  润新知