大聪明教你学Java | Spring Boot 使用自定义注解实现防止表单重复提交

前言

这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战

表单重复提交是在多用户的 Web 应用中最常见且带来麻烦最多的一个问题。有很多的应用场景都会遇到表单重复提交问题,比如由于用户误操作,多次点击表单提交按钮;由于网速等原因造成页面卡顿,用户重复刷新提交页面,甚至会有黑客或恶意用户使用工具重复恶意提交表单来对网站进行攻击,所以说防止表单重复提交在 Web 应用中的重要性是极高的。

今天就和大家分享一下如何利用自定义注解来实现防止表单重复提交✌。

使用自定义注解实现防止表单重复提交

我们还是先引入 Maven 依赖👇

<!-- Spring框架基本的核心工具 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>

<!-- SpringWeb模块 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
</dependency>
<!-- 自定义验证注解 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--常用工具类 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>
<!-- io常用工具类 -->
<dependency>
     <groupId>commons-io</groupId>
     <artifactId>commons-io</artifactId>
 </dependency>
<!-- JSON工具类 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
</dependency>
<!-- servlet包 -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
</dependency>
复制代码

接下来我们实现一个注解类(注解在 Java 中与类、接口的声明类似,只是所使用的关键字有所不同,声明注解使用 @interface 关键字。在底层实现上,所有定义的注解都会自动继承 java.lang.annotation.Annotation 接口。)

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义注解防止表单重复提交
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{
    /**
     * 间隔时间(ms),小于此时间视为重复提交
     */
    public int interval() default 5000;

    /**
     * 提示消息
     */
    public String message() default "不允许重复提交,请稍后再试";
}
复制代码

P.S. 关于自定义注解的解释在之前的博客中有写到,这里就不再多说了,有需要了解的小伙伴请移步至:大聪明教你学Java | Spring Boot 使用自定义注解实现操作日志的记录

下面我们再实现一个拦截器,对提交表单的过程做一个相应的拦截校验👇

import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * 防止重复提交拦截器
 */
@Component
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
{
    @Override
    public 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))
                {
                    //如果本次提交被认为是重复提交,则在此处做具体的逻辑处理
                    //如:弹出警告窗口等
                    return false;
                }
            }
            return true;
        }
        else
        {
            return true;
        }
    }

    /**
     * 验证是否重复提交由子类实现具体的防重复提交的规则
     *
     * @param request 请求对象
     * @param annotation 防复注解
     * @return 结果
     */
    public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception;
}
复制代码

我们可以看到再拦截器中一共有两个方法,分别是 preHandle 和 isRepeatSubmit。无论我们执行什么请求,都会进入 RepeatSubmitInterceptor 拦截器,进入拦截器后先执行 preHandle 方法进行预处理,判断本次拦截的方法是否增加了 RepeatSubmit 自定义注解,如果增加了该注解才会进行具体的校验。isRepeatSubmit 方法是防止表单重复提交的规则,我们通过子类来实现 isRepeatSubmit 具体的校验规则👇

import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import com.mumu.framework.interceptor.RepeatSubmitInterceptor;
import org.springframework.stereotype.Component;
import com.mumu.common.annotation.RepeatSubmit;
import com.xxx.xxx.xxx.JSON; 

/**
 * 判断请求url和数据是否和上一次相同
 * com.xxx.xxx.xxx.JSON; 是自定义的json工具类,具体代码贴在后面
 */
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{
    public final String REPEAT_PARAMS = "repeatParams";

    public final String REPEAT_TIME = "repeatTime";

    public final String SESSION_REPEAT_KEY = "repeatData";

    @SuppressWarnings("unchecked")
    @Override
    public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception
    {
        // 本次参数及系统时间
        String nowParams = JSON.marshal(request.getParameterMap());
        Map<String, Object> nowDataMap = new HashMap<String, Object>();
        nowDataMap.put(REPEAT_PARAMS, nowParams);
        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());

        // 请求地址(作为存放session的key值)
        String url = request.getRequestURI();

        HttpSession session = request.getSession();
        Object sessionObj = session.getAttribute(SESSION_REPEAT_KEY);
        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 (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval()))
                {
                    return true;
                }
            }
        }
        Map<String, Object> sessionMap = new HashMap<String, Object>();
        sessionMap.put(url, nowDataMap);
        session.setAttribute(SESSION_REPEAT_KEY, sessionMap);
        return false;
    }

    /**
     * 判断参数是否相同
     */
    private 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);
    }

    /**
     * 判断两次间隔时间
     */
    private 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;
    }
}
复制代码

自定义 Json 工具类👇

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;

/**
 * JSON解析处理
 */
public class JSON
{
    private static final ObjectMapper objectMapper = new ObjectMapper();
    private static final ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter();

    public static void marshal(File file, Object value) throws Exception
    {
        try
        {
            objectWriter.writeValue(file, value);
        }
        catch (JsonGenerationException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }

    public static void marshal(OutputStream os, Object value) throws Exception
    {
        try
        {
            objectWriter.writeValue(os, value);
        }
        catch (JsonGenerationException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }

    public static String marshal(Object value) throws Exception
    {
        try
        {
            return objectWriter.writeValueAsString(value);
        }
        catch (JsonGenerationException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }

    public static byte[] marshalBytes(Object value) throws Exception
    {
        try
        {
            return objectWriter.writeValueAsBytes(value);
        }
        catch (JsonGenerationException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }

    public static <T> T unmarshal(File file, Class<T> valueType) throws Exception
    {
        try
        {
            return objectMapper.readValue(file, valueType);
        }
        catch (JsonParseException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }

    public static <T> T unmarshal(InputStream is, Class<T> valueType) throws Exception
    {
        try
        {
            return objectMapper.readValue(is, valueType);
        }
        catch (JsonParseException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }

    public static <T> T unmarshal(String str, Class<T> valueType) throws Exception
    {
        try
        {
            return objectMapper.readValue(str, valueType);
        }
        catch (JsonParseException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }

    public static <T> T unmarshal(byte[] bytes, Class<T> valueType) throws Exception
    {
        try
        {
            if (bytes == null)
            {
                bytes = new byte[0];
            }
            return objectMapper.readValue(bytes, 0, bytes.length, valueType);
        }
        catch (JsonParseException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }
}

复制代码

在校验的过程中主要针对三个部分做校验:请求地址、请求参数以及请求时间,只有当请求地址和请求参数一致且两次请求的时间间隔小于xx毫秒时(xx毫秒即为在自定义注解中指定的时间),才会被判定为是重复提交。这种校验方式较为繁琐(毕竟需要比对三部分内容),但是它可以尽可能的避免出现“误拦截”的情况,让系统有更高的可用性和安全性。

本文中提到的方法只是众多方法中的一种,我个人也是通过这种方式来实现的防止表单重复提交。当然了,我们可以选择的办法有很多,比如采用 JS 禁用提交按钮、利用Session防止表单重复提交等等,最终如何选择还是要根据自己的应用和开发框架来决定,选择一个适合自己的方法才是最好的💪。

小结

本人经验有限,有些地方可能讲的没有特别到位,如果您在阅读的时候想到了什么问题,欢迎在评论区留言,我们后续再一一探讨🙇‍

希望各位小伙伴动动自己可爱的小手,来一波点赞+关注 (✿◡‿◡) 让更多小伙伴看到这篇文章~ 蟹蟹呦(●'◡'●)

如果文章中有错误,欢迎大家留言指正;若您有更好、更独到的理解,欢迎您在留言区留下您的宝贵想法。

爱你所爱 行你所行 听从你心 无问东西

来源:https://juejin.cn/post/7064458289261576205
posted @ 2022-10-05 22:54  程序员小明1024  阅读(226)  评论(0编辑  收藏  举报