浅谈拦截器在防止请求的重复提交中的应用


思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜


浅谈SpringMVC中的拦截器在防止请求的重复提交中的应用。

注意:本文只是抛砖引玉式的简单谈一谈SpringMVC中拦截器在防止请求重复提交中的应用,因此文中提供案例所提供代码可能很简陋,所以请勿直接应用于项目!

前言

在分析之前,我们先来看下什么是请求的重复提交。

请求重复提交通常是指用户在操作时多次多次发送相同请求,进而导致相同请求被服务器处理多次。更进一步,当请求不断重复提交后,还可能导致如下问题:

  1. 数据一致性问题: 如果一个表单提交包含对数据库或后端资源的修改操作,重复提交可能导致数据不一致。例如,如果用户多次提交相同的订单或支付请求,可能会导致多次扣款或重复生成订单。
  2. 资源浪费: 处理重复请求可能会浪费服务器资源和带宽。服务器需要处理相同的请求多次,而客户端需要多次下载和显示相同的响应。
  3. 系统性能问题: 大规模的请求重复提交可能会对服务器性能产生负面影响,尤其是在高负载情况下。服务器可能会因处理重复请求而变得不稳定。

因此,为了防止请求的重复提交,应采取适当的措施来避免请求被重复提交。

防止请求重复提交的常见手段

通过之前的分析不难发现,请求的重复的本质就是一个本应该发送一次的请求,因为操作不当而导致发送了多次,进而导致了请求的重复提交。在Web业务开发中,为了应对这一问题通常可以采取如下手段来进行预防:

  1. Token验证: 在服务器端生成一个唯一的令牌(Token),将其包含在响应中并存储在前端(通常在表单中的隐藏字段或者请求头中)。在接收到请求时,服务器验证令牌的唯一性,如果令牌已被使用过,拒绝重复请求。
  2. 过期时间: 在前端和后端都可以设置一个请求的有效时间戳。当请求被发送到服务器时,服务器可以检查请求的时间戳,如果请求时间戳过期,拒绝处理请求。
  3. 数据库记录: 在服务器端,可以将已经处理的请求记录在数据库中,通过检查请求是否已存在来防止重复请求。这种方法适用于处理重复数据写入数据库的情况。

其实无论采用什么方法,其核心的本质就是一段时间内仅能允许请求被提交一次。而为了达到这一目的最好的办法就是为请求添加一个唯一标示。因此无论是Token还是数据库记录,其核心都在于借助外界的力量来为无状态http请求构建状态。

总之,防止请求重复提交的关键点在于确保请求的唯一性。当理解了这一点后,其实也就把握住了问题的关键,这样后续即使接触到其他防止请求重复处理的方式,也能快速上手,所以在学习过程中一定要找到问题的本质逻辑,不要将太多精力放在技巧上,更应关注事物的底层了逻辑。

说了这么多,我们来看看如何通过SpringMVC的拦截器如何在防止请求重复提交中发挥作用。

利用拦截器避免请求重复提交

java 复制代码
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashSet;
import java.util.Set;

public class DuplicateSubmitInterceptor 
            implements HandlerInterceptor {
    /**
    * 存放Token的集合
    **/
    private Set<String> tokens = new HashSet<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getParameter("token"); // 从请求参数中获取token
        if (token != null) {
            if (tokens.contains(token)) {
                // 如果token已经存在,说明是重复提交,拦截请求
                return false;
            }
            // 否则,将token添加到集合中
            tokens.add(token);
        }
        return true;
    }
}

在上述示例中,我们创建了一个DuplicateSubmitInterceptor拦截器,它使用tokens集合来保存已经提交的token。如果当前Token已经在集合中,则表明当前用户已经提交相关请求,后续请求则不会到达Controller层进行单独处理。

(注:如果你对SpringMVC中拦截器的工作原理有所疑惑可参考笔者之前的SpringMVC流程分析(五):HandlerInterceptor组件分析责任链模式在SpringMVC中的应用 进行了解!)

上述代码其实已经可以完成任务了,但是不知道是否考虑过这样的一个问题。即前台当请求过多时DuplicateSubmitInterceptor中的tokens会缓存大量信息,这非常占用程序内存大小。同时其中的好多token信息其实是无效的,一个比较不错的方法就是定时清除!

为了实现定时清除,在SpringBoot的应用中我们可以通过@Scheduled注解来创建一个定时任务。引入@Scheduled注解后,代码变为如下形式:

java 复制代码
@Component
public class DuplicateSubmitInterceptor implements HandlerInterceptor {
    private Set<String> tokens = new HashSet<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getParameter("token");
        if (token != null) {
            if (tokens.contains(token)) {
                // 如果token已经存在,说明是重复提交,拦截请求
                return false;
            }
            // 否则,将token添加到集合中并设置有效期
            tokens.add(token);
        }
        return true;
    }

    @Scheduled(fixedRate = 1800000) // 每30分钟执行一次
    public void cleanUpExpiredTokens() {
        long currentTime = System.currentTimeMillis();
        tokens.removeIf(token -> isTokenExpired(token, currentTime));
    }

    private boolean isTokenExpired(String token, long currentTime) {
        // 根据Token的创建时间和过期时间判断Token是否过期
        // 这里假设Token的有效期是30分钟
        // 实际情况中,你可以根据需求自定义Token的有效期判断逻辑
        // 这里使用了毫秒作为时间单位
        long tokenCreationTime = Long.parseLong(token); // 假设Token中存储了创建时间
        long tokenExpirationTime = tokenCreationTime + TimeUnit.MINUTES.toMillis(30); // 30分钟有效期
        return currentTime > tokenExpirationTime;
    }
}

在上述代码中,我们创建了一个cleanUpExpiredTokens方法,它会每隔30分钟执行一次,用于清理过期的Token。进一步,isTokenExpired方法用于判断Token是否过期,过期的Token将从tokens集合中移除。

总结

在如下图所示的MVC架构中,只要你愿意你可以将防止请求重复提交的校验逻辑写在控制层或者服务层中,也不一定非常通过SpringMVC中的拦截器实现,但这样做的弊端就是控制层服务层的功能划分变的臃肿,这其实与MVC分层划分的思想是背道而驰的。

再回到今天我们所讨论的话题,其实你会发现,我们在通过拦截器实现时,其本质思想就是为Http请求创建唯一标示,然后进行唯一性校验。为此我们在拦截器中通过一个set集合来存储标示,并进行判断。进一步,其可能诱发的问题就是内存溢出。

为了解决这一问题,我们引入了定时清除机制来进行代码的优化。不知道你有没有发现这样的做法其实与Redis的处理思想很类似?事实上,很多知识间都是连通的,其永远不是孤立的。希望文章对你学习有所启发。

最后,如果觉得文章对你有帮助的话,不妨点赞加关注,不错过笔者之后的每一次更新!

相关推荐
Chrikk1 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*1 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue1 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man1 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
测开小菜鸟1 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity2 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天2 小时前
java的threadlocal为何内存泄漏
java
caridle2 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^3 小时前
数据库连接池的创建
java·开发语言·数据库
苹果醋33 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx