Java后台防止请求重复提交,拦截器+注解实现防止表单重复提交

一、前言

由于网络原因,用户操作有误(连续点击两次以上提交按钮),或者页面卡顿等原因,可能会出现请求重复提交,造成数据库保存多条重复数据。后端实现拦截器防重。

那么如何防止请求重复提交呢?一般有两种解决方案:
第一种:前端处理,在提交完成之后,将按钮禁用。
第二种:后端处理,使用拦截器拦截。

交给前端解决,判断多长时间内不能再次点击按钮,或者点击之后禁用按钮,当然,聪明的小伙伴能够绕过前端验证,因此推荐后端进行拦截处理。

二、实现思路

使用拦截器防止请求重复提交,本文模仿若依防重给大家分享,利用 AOP 切面在进入方法前拦截,通过 Session 或 Redis 的 key-value 键值对存储,指定 key+url+消息头 来拼成字符串组成 key,使用 请求参数+时间 封装 map 对象赋值 value,当 key 不存在时,则为新的请求;若存在,则对请求参数以及请求的间隔时间进行判断是否重复提交。

2.1、自定义注解防止表单重复提交

package com.dian.jiao.interceptor.annotation;import java.lang.annotation.*;/*** 自定义注解防止表单重复提交*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {/*** 间隔时间(ms),小于此时间视为重复提交*/public int interval() default 5000;/*** 提示消息*/public String message() default "不允许重复提交,请稍候再试";
}

2.2、构建包装器

package com.dian.jiao.interceptor.wrapper;import com.dian.jiao.util.ServletUtils;import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;/*** 构建可重复读取inputStream的request*/
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {public final String UTF8 = "UTF-8";private final byte[] body;public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException {super(request);request.setCharacterEncoding(UTF8);response.setCharacterEncoding(UTF8);body = ServletUtils.getBodyString(request).getBytes(UTF8);}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException {final ByteArrayInputStream bais = new ByteArrayInputStream(body);return new ServletInputStream() {@Overridepublic int read() throws IOException {return bais.read();}@Overridepublic int available() throws IOException {return body.length;}@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener readListener) {}};}
}

2.3、防止重复提交拦截器

package com.dian.jiao.interceptor;import com.dian.jiao.interceptor.annotation.RepeatSubmit;
import com.dian.jiao.interceptor.wrapper.RepeatedlyRequestWrapper;
import com.dian.jiao.pojo.User;
import com.dian.jiao.util.CommonUtils;
import com.dian.jiao.util.ServletUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;/*** 防止重复提交拦截器*/
public class RepeatSubmitInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (handler instanceof HandlerMethod) {// 处理器方法HandlerMethod handlerMethod = (HandlerMethod) handler;// 获取处理方法中的注册方法Method method = handlerMethod.getMethod();// 从注册方法中获取到自定义注解RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);// 判断注解是否存在if (annotation != null) {// 判断是否重复提交if (this.isRepeatSubmit(request, annotation)) {// 将弹框字符串渲染到客户端ServletUtils.alert(response, annotation.message());return false;}}}return true;}/*** 验证是否重复提交,实现具体的防重复提交的规则*/@SuppressWarnings("unchecked")private boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) {HttpSession session = request.getSession();User user = (User) session.getAttribute("loginUser");String nowParams = "";if (request instanceof RepeatedlyRequestWrapper) {RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;nowParams = ServletUtils.getBodyString(repeatedlyRequest);}// body参数为空,获取Parameter的数据if (nowParams == null || "".equals(nowParams.trim()) {nowParams = CommonUtils.toJSONString(request.getParameterMap());}Map<String, Object> nowDataMap = new HashMap<>(2);nowDataMap.put(CommonUtils.REPEAT_PARAMS, nowParams);nowDataMap.put(CommonUtils.REPEAT_TIME, System.currentTimeMillis());// 请求地址(作为存放cache的key值)String url = request.getRequestURI();// 用户IDString submitKey = user == null ? ServletUtils.getIpAddr(request) : String.valueOf(user.getId());// 唯一标识(指定key + url + 消息头)String cacheRepeatKey = CommonUtils.REPEAT_SUBMIT_KEY + url + submitKey;Object sessionObj = session.getAttribute(cacheRepeatKey);if (sessionObj != null) {Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;if (sessionMap.containsKey(url)) {Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);if (CommonUtils.compareParams(nowDataMap, preDataMap) && CommonUtils.compareTime(nowDataMap, preDataMap, annotation.interval())) {return true;}}}Map<String, Object> cacheMap = new HashMap<>(1);cacheMap.put(url, nowDataMap);session.setAttribute(cacheRepeatKey, cacheMap);return false;}
}

2.4、客户端工具类

package com.dian.jiao.util;import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.nio.charset.StandardCharsets;/*** 客户端工具类*/
public class ServletUtils {/*** 获取body请求参数* @param request 请求对象{@link ServletRequest}* @return String*/public static String getBodyString(ServletRequest request) {StringBuilder sb = new StringBuilder();BufferedReader reader = null;try (InputStream inputStream = request.getInputStream()) {reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));String line = "";while ((line = reader.readLine()) != null) {sb.append(line);}} catch (IOException e) {e.printStackTrace();} finally {if (reader != null) {try {reader.close();} catch (IOException e) {e.printStackTrace();}}}return sb.toString();}/*** 将弹框字符串渲染到客户端** @param response 渲染对象* @param msg 待渲染的弹框字符串*/public static void alert(HttpServletResponse response, String msg) {try {response.reset();response.setHeader("Content-type", "text/html;charset=UTF-8");response.setCharacterEncoding("utf-8");PrintWriter out = response.getWriter();out.print("<script>");out.print("alert(\"" + msg + "\");");out.print("history.back();");out.print("</script>");} catch (IOException e) {e.printStackTrace();}}/*** 获取客户端IP* * @param request 请求对象* @return IP地址*/public static String getIpAddr(HttpServletRequest request) {if (request == null) {return "unknown";}String ip = request.getHeader("x-forwarded-for");if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("X-Forwarded-For");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("WL-Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("X-Real-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);}/*** 从多级反向代理中获得第一个非unknown IP地址** @param ip 获得的IP地址* @return 第一个非unknown IP地址*/public static String getMultistageReverseProxyIp(String ip) {// 多级反向代理检测if (ip != null && ip.indexOf(",") > 0) {final String[] ips = ip.trim().split(",");for (String subIp : ips) {if (false == isUnknown(subIp)) {ip = subIp;break;}}}return StringUtils.substring(ip, 0, 255);}/*** 检测给定字符串是否为未知,多用于检测HTTP请求相关** @param checkString 被检测的字符串* @return 是否未知*/public static boolean isUnknown(String checkString) {return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);}
}

2.5、公共工具类

package com.dian.jiao.util;import com.fasterxml.jackson.databind.ObjectMapper;import java.util.Map;public class CommonUtils {public static final String REPEAT_PARAMS = "repeatParams";public static final String REPEAT_TIME = "repeatTime";public static final String REPEAT_SUBMIT_KEY = "repeat_submit:";/*** 判断参数是否相同*/public static boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) {String nowParams = (String) nowMap.get(REPEAT_PARAMS);String preParams = (String) preMap.get(REPEAT_PARAMS);return nowParams.equals(preParams);}/*** 判断两次间隔时间*/public static boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval) {long time1 = (Long) nowMap.get(REPEAT_TIME);long time2 = (Long) preMap.get(REPEAT_TIME);if ((time1 - time2) < interval) {return true;}return false;}public static String toJSONString(Object object) {if (object != null) {try {return new ObjectMapper().writeValueAsString(object);} catch (Exception e) {e.printStackTrace();}}return null;}
}

2.6、配置springmvc-servlet.xml,添加拦截器

<mvc:interceptors><mvc:interceptor><mvc:mapping path="/**"/><!-- 配置不拦截的请求 --><mvc:exclude-mapping path="/login"/><mvc:exclude-mapping path="/getCode"/><bean class="com.dian.jiao.interceptor.RepeatSubmitInterceptor"/></mvc:interceptor>
</mvc:interceptors>

三、使用教程

在接口方法上添加 @RepeatSubmit 注解即可,注解参数说明:

参数类型默认值描述
intervalint5000间隔时间(ms),小于此时间视为重复提交
messageString不允许重复提交,请稍后再试提示消息

示例1:采用默认参数

@RepeatSubmit
public AjaxResult addSave()
{return  AjaxResult.success();
}

示例2:指定防重复时间和错误消息

@RepeatSubmit(interval = 3000, message = "您已经报名,不能重复报名")
public AjaxResult addSave()
{return  AjaxResult.success();
}

大功告成!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/149939.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Apache Pulsar 技术系列 - 基于 Pulsar 的海量 DB 数据采集和分拣

导语 Apache Pulsar 是一个多租户、高性能的服务间消息传输解决方案&#xff0c;支持多租户、低延时、读写分离、跨地域复制、快速扩容、灵活容错等特性。本文是 Pulsar 技术系列中的一篇&#xff0c;主要介绍 Pulsar 在海量DB Binlog 增量数据采集、分拣场景下的应用。 前言…

程序员开发者神器:10个.Net开源项目

今天一起盘点下&#xff0c;8月份推荐的10个.Net开源项目&#xff08;点击标题查看详情&#xff09;。 1、基于C#开发的适合Windows开源文件管理器 该项目是一个基于C#开发、开源的文件管理器&#xff0c;适用于Windows&#xff0c;界面UI美观、方便轻松浏览文件。此外&#…

课程设计:C++实现哈夫曼编码

功能实现&#xff1a; //1:先计算每个字符的权重//2&#xff1a;构建哈夫曼树//3&#xff1a;得出每个字符的哈夫曼编码。//4:根据哈夫曼编码转化为字符 代码实现&#xff1a; // 哈夫曼编码.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 //1:先计…

【如何让你的建筑设计更高效】推荐7个3DMAX建筑设计的实用插件

3DMAX是创建具有复杂对象和照片级真实感材质的大型三维项目的绝佳工具。它有用于粒子模拟和参数化建模的内置工具&#xff0c;只要有足够的时间和练习&#xff0c;你就可以创建任何东西。然而&#xff0c;总有改进的余地。许多第三方开发人员已经发布了自己的扩展&#xff0c;也…

YOLOv8优化策略:轻量级Backbone改进 | VanillaNet极简神经网络模型 | 华为诺亚2023

🚀🚀🚀本文改进:一种极简的神经网络模型 VanillaNet,支持vanillanet_5, vanillanet_6, vanillanet_7, vanillanet_8, vanillanet_9, vanillanet_10, vanillanet_11等版本 🚀🚀🚀YOLOv8改进专栏:http://t.csdnimg.cn/hGhVK 学姐带你学习YOLOv8,从入门到创新,…

Spring高级bean的实例化方法

bean的实例化方法 构造方法 实例化bean第一种&#xff1a;使用默认无参构造函数(常用) 第二种创建bean实例&#xff1a;静态工厂实例化&#xff08;了解&#xff09; 第三种&#xff1a;实例工厂&#xff08;了解&#xff09;与FactoryBean&#xff08;实用&#xff09;

AQS原理

文章目录 1. 简介2. 基于AQS实现自定义锁 1. 简介 AQS时AbstractQueueSynchronizer&#xff0c;是阻塞式锁的同步器工具的框架。AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器。特点如下&#xff1a; 用State属性来表示资源的状态…

JVM判断对象是否存活之引用计数法、可达性分析

目录 前言 引用计数法 概念 优点 缺点 可达性分析 概念 缺点&#xff1a; 扩展&#xff1a; 1.GC Roots 概念 2.STW (Stop the world) 前言 JVM有两种算法来判断对象是否存活&#xff0c;分别是引用计数法和可达性分析算法&#xff0c;针对可达性分析算法STW时间长、…

【MySQL】_JDBC

目录 1. JDBC原理 2. 导入JDBC驱动包 3. 编写JDBC代码实现Insert 3.1 创建并初始化一个数据源 3.2 和数据库服务器建立连接 3.3 构造SQL语句 3.4 执行SQL语句 3.5 释放必要的资源 4. JDBC代码的优化 4.1 从控制台输入 4.2 避免SQL注入的SQL语句 5. 编写JDBC代码实现…

第二次授课内容

1、第二次课程内容讲评。 服务枚举 服务的二进制的路径获取这块&#xff0c;对于代理执行这种类型的服务&#xff0c;枚举结果这是不正确&#xff0c;&#xff08;同步读取文件可能导致&#xff0c;文件打开失败。服务可能带有比较高的权限&#xff1b;独享式打开的时候&…

ArrayList中放的是一个对象,如何同时根据对象中的三个字段对List进行排序

import java.util.ArrayList; import java.util.Collections; import java.util.Comparator;public class YourObject {private int field1;private String field2;private double field3;// 构造函数和其他代码public int getField1() {return field1;}public String getField…

3298:练50.1 查分程序《信息学奥赛一本通编程启蒙(C++版)》

3298&#xff1a;练50.1 查分程序《信息学奥赛一本通编程启蒙&#xff08;C版&#xff09;》 【题目描述】 尼克&#xff0c;格莱尔等5位同学进行了一次信息学测试&#xff0c;试编一程序&#xff0c;实现查分功能。先输入成绩&#xff0c;然后输入学号输入相应的成绩。 【输…

〖大前端 - 基础入门三大核心之JS篇㊲〗- DOM改变元素节点的css样式、HTML属性

说明&#xff1a;该文属于 大前端全栈架构白宝书专栏&#xff0c;目前阶段免费&#xff0c;如需要项目实战或者是体系化资源&#xff0c;文末名片加V&#xff01;作者&#xff1a;不渴望力量的哈士奇(哈哥)&#xff0c;十余年工作经验, 从事过全栈研发、产品经理等工作&#xf…

Python数据分析实战① Python实现数据可视化

文章目录 一、数据可视化介绍二、matplotlib和pandas画图1.matplotlib简介和简单使用2.matplotlib常见作图类型3.使用pandas画图4.pandas中绘图与matplotlib结合使用 三、订单数据分析展示四、Titanic灾难数据分析显示 一、数据可视化介绍 数据可视化是指将数据放在可视环境中…

.Net中Redis的基本使用

前言 Redis可以用来存储、缓存和消息传递。它具有高性能、持久化、高可用性、扩展性和灵活性等特点&#xff0c;尤其适用于处理高并发业务和大量数据量的系统&#xff0c;它支持多种数据结构&#xff0c;如字符串、哈希表、列表、集合、有序集合等。 Redis的使用 安装包Ser…

wpf devexpress 排序、分组、过滤数据

这个教程示范在GridControl如何排序数据&#xff0c;分组数据给一个行创建一个过滤。这个教程基于前一个教程。 排序数据 可以使用GridControl 排序数据。这个例子如下过滤数据对于Order Date 和 Customer Id 行&#xff1a; 1、对于Order Date 和 Customer Id 行指定Colum…

代码随想录算法训练营Day 55 || 583. 两个字符串的删除操作、72. 编辑距离

583. 两个字符串的删除操作 力扣题目链接(opens new window) 给定两个单词 word1 和 word2&#xff0c;找到使得 word1 和 word2 相同所需的最小步数&#xff0c;每步可以删除任意一个字符串中的一个字符。 示例&#xff1a; 输入: "sea", "eat"输出: …

Linux虚拟机中网络连接的三种方式

Linux 虚拟机中网络连接的三种方式 先假设一个场景&#xff0c;在教室中有三个人&#xff1a;张三、李四和王五&#xff08;这三个人每人有一台主机&#xff09;&#xff0c;他们三个同处于一个网段中&#xff08;192.169.0.XX&#xff09;&#xff0c;也就是说他们三个之间可…

深度学习_13_YOLO_图片切片及维度复原

需求&#xff1a; 在对获取的图片进行识别的时候&#xff0c;如果想减少不必要因素的干扰&#xff0c;将图片切割只对有更多特征信息的部分带入模型识别&#xff0c;而剩余有较多干扰因素的部分舍弃&#xff0c;这就是图片切割的目的&#xff0c;但是又由于模型对图片的维度有较…

计数排序.

一.定义&#xff1a; 计数排序&#xff08;Counting Sort&#xff09;是一种非比较性质的排序算法&#xff0c;其时间复杂度为O(nk)&#xff08;其中n为待排序的元素个数&#xff0c;k为不同值的个数&#xff09;。这意味着在数据值范围不大并且离散分布的情况下&#xff0c;规…