避免重复提交表单

本文介绍了一种防止Web表单重复提交的有效方法,包括利用前端JavaScript禁用提交按钮及后端实现Token验证机制。Token验证涉及单例模式、MD5加密等技术,确保表单提交的安全性。

重复提交表单原因:

1.前台多次按了“提交”接钮;

通过在前台按钮的onclick事件来避免

 <script language="javascript">
            <!--
                var checkSubmitFlg=true; 
                function checkSubmit()
                {
                    if(true==checkSubmitFlg)
                    {
                        document.theForm.submit();
                        checkSubmitFlg=false;
                    }
                    else
                    {
                        alert("你已经提交了表单,请不要重复提交!");
                    }
                }
            //-->
        </script>

 

 

 

2.在浏览器按“刷新”按钮

在相关的页面的表单上增加TokenProcessor隐藏域(input type="hidden")。

(1)在后台建立TokenProcessor类

package com.cjg.servlet;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * TokenProcessor类是一个单例类。
*/
public class TokenProcessor
{
    static final String TOKEN_KEY="org.sunxin.token";
   
    private static TokenProcessor instance = new TokenProcessor();

    /**
     * getInstance()方法得到单例类的实例。
     */
    public static TokenProcessor getInstance()
    {
        return instance;
    }

    /**
     * 最近一次生成令牌值的时间戳。
     */
    private long previous;

    /**
     * 判断请求参数中的令牌值是否有效。
     */
    public synchronized boolean isTokenValid(HttpServletRequest request)
    {
        //得到请求的当前Session对象。
        HttpSession session = request.getSession(false);
        if (session == null)
        {
            return false;
        }
        
        //从Session中取出保存的令牌值。
        String saved = (String) session.getAttribute(TOKEN_KEY);
        if (saved == null) {
            return false;
        }
        
        //清除Session中的令牌值。
        resetToken(request);
        
        
        //得到请求参数中的令牌值。
        String token = request.getParameter(TOKEN_KEY);
        if (token == null) {
            return false;
        }
        
        return saved.equals(token);
    }

    /**
     * 清除Session中的令牌值。
     */
    public synchronized void resetToken(HttpServletRequest request)
    {

        HttpSession session = request.getSession(false);
        if (session == null) {
            return;
        }
        session.removeAttribute(TOKEN_KEY);
    }

    /**
     * 产生一个新的令牌值,保存到Session中,
     * 如果当前Session不存在,则创建一个新的Session。
     */
    public synchronized void saveToken(HttpServletRequest request)
    {

        HttpSession session = request.getSession();
        String token = generateToken(request);
        if (token != null) {
            session.setAttribute(TOKEN_KEY, token);
        }

    }

    /**
     * 根据用户会话ID和当前的系统时间生成一个唯一的令牌。
     */
    public synchronized String generateToken(HttpServletRequest request)
    {

        HttpSession session = request.getSession();
        try
        {
            byte id[] = session.getId().getBytes();
            long current = System.currentTimeMillis();
            if (current == previous)
            {
                current++;
            }
            previous = current;
            byte now[] = new Long(current).toString().getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(id);
            md.update(now);
            return toHex(md.digest());
        }
        catch (NoSuchAlgorithmException e)
        {
            return null;
        }
    }

    /**
     * 将一个字节数组转换为一个十六进制数字的字符串。
     */
    private String toHex(byte buffer[])
    {
        StringBuffer sb = new StringBuffer(buffer.length * 2);
        for (int i = 0; i < buffer.length; i++)
        {
            sb.append(Character.forDigit((buffer[i] & 0xf0) >> 4, 16));
            sb.append(Character.forDigit(buffer[i] & 0x0f, 16));
        }
        return sb.toString();
    }
    
    /**
     * 从Session中得到令牌值,如果Session中没有保存令牌值,则生成一个新的令牌值。
     */
    public synchronized String getToken(HttpServletRequest request)
    {
        HttpSession session = request.getSession(false);
        if(null==session)
            return null;
        String token=(String)session.getAttribute(TOKEN_KEY);
        if(null==token)
        {
           token = generateToken(request);
            if (token != null)
            {
                session.setAttribute(TOKEN_KEY, token);
                return token;
            }
            else
                return null;
        }
        else
            return token;
    }
}

 (2)在表单加入TokenProcessor隐藏域

 <body>
        <%
            TokenProcessor processor=TokenProcessor.getInstance(); 
			String token=processor.getToken(request);
        %>
        <form method="post" action="handler" name="theForm">
            <table>
                <tr>
                    <td>用户名:</td>
                    <td><input type="text" name="username"></td>
                </tr>
                <tr>
                    <td>密码:</td>
    	            <td>
    	                <input type="password" name="password">
                        <input type="hidden" name="org.sunxin.token" value="<%=token%>"/>
    	            </td>
    	        </tr>
    	        <tr>
    	            <td><input type="reset" value="重填"></td>
    	            <td><input type="button" name="btnSubmit" value="提交" onClick="checkSubmit();"/></td>
    	        </tr>
	        </table>
	        
        </form>
    </body>

 (3)处理form的Servlet

 

package com.cjg.servlet;

import javax.servlet.*;
import java.io.*;
import javax.servlet.http.*;
import java.util.*;

import com.cjg.servlet.TokenProcessor;

public class HandlerServlet extends HttpServlet
{
    int count=0;
    public void doPost(HttpServletRequest req, HttpServletResponse resp)
               throws ServletException,IOException
    {
        resp.setContentType("text/html;charset=GBK");
        PrintWriter out=resp.getWriter();
        
        TokenProcessor processor=TokenProcessor.getInstance();
        if(processor.isTokenValid(req))
        {
            try
            {
                Thread.sleep(5000);
            }
            catch(InterruptedException e)
            {
                System.out.println(e);
            }
                
            System.out.println("submit : "+count);
            if(count%2==1)
                count=0;
            else
                count++;
            out.println("success");
        }
        else
        {
            processor.saveToken(req);
            out.println("你已经提交了表单,同一表单不能提交两次。");
        }
        out.close();
    }
}

 

 

 

 

### 防止用户重复提交表单的最佳实践与实现方法 在企业级项目中,防止表单重复提交是一个关键问题,尤其是在订单提交、支付处理等关键业务场景中。重复提交可能导致数据库产生重复数据、业务逻辑异常(如积分多次扣除)、服务器资源浪费等问题。为了解决这一问题,可以从**前端控制**和**后端控制**两个层面入手,结合技术手段和业务逻辑进行综合处理。 #### 前端控制策略 前端控制是防止重复提交的第一道防线。通过限制用户操作,可以有效减少因用户多次点击提交按钮导致的重复请求。常见的实现方式包括: - **禁用提交按钮**:在用户点击提交按钮后,立即禁用该按钮,防止用户再次点击。例如: ```javascript document.getElementById("submitBtn").disabled = true; ``` - **使用防抖机制**:通过设置一个时间窗口(如500毫秒),确保在该时间窗口内只执行一次提交操作。例如: ```javascript let isSubmitting = false; function submitForm() { if (isSubmitting) return; isSubmitting = true; // 提交逻辑... } ``` 这些方法可以有效防止用户因网络延迟或误操作导致的重复提交行为[^2]。 #### 后端控制策略 虽然前端控制可以缓解问题,但并不能完全防止重复提交。为了确保数据一致性,必须在后端实现更严格的防重机制。常见方案包括: - **Token机制**:在用户打开表单页面时生成一个唯一的Token,并将其存储在服务端(如Redis、Session等)。用户提交表单时需携带该Token,服务端在接收到请求后验证Token是否存在,若存在则处理请求并删除Token,防止再次使用。例如在Spring Boot中可使用拦截器实现该逻辑: ```java @Component public class RepeatSubmitInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getParameter("token"); if (token == null || !RedisUtil.exists(token)) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "重复提交"); return false; } RedisUtil.delete(token); return true; } } ``` - **幂等性设计**:对关键接口进行幂等性改造,确保相同的请求多次执行结果一致。例如使用唯一业务标识(如订单号)作为幂等键,在处理请求前检查该键是否已存在,若存在则跳过处理。该方式适用于分布式系统中的重复提交控制[^3]。 - **数据库唯一约束**:在数据库层面设置唯一索引,例如订单号字段设置为唯一索引,当重复提交导致插入相同订单号时,数据库会抛出异常,从而阻止重复数据的产生。 - **分布式锁机制**:在高并发场景下,可使用Redis分布式锁对关键操作进行加锁,确保同一时间只有一个请求能执行提交操作。例如: ```java Boolean isLocked = redisTemplate.opsForValue().setIfAbsent("lock:order:" + orderId, "locked", 5, TimeUnit.SECONDS); if (isLocked != null && isLocked) { try { // 执行提交逻辑 } finally { redisTemplate.delete("lock:order:" + orderId); } } else { throw new RuntimeException("请勿重复提交"); } ``` #### 综合实践建议 为了实现更高效的防重复提交机制,建议采用**前后端结合**的方式。前端负责限制用户操作,后端则通过Token机制、幂等性设计、数据库约束等手段确保系统层面的防重能力。对于高并发系统,推荐使用Redis进行Token管理或分布式锁控制,以提升系统响应速度和稳定性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值