防止表单重复提交的几种方式,演示一个自定义注解方式的实现

一、防止表单重复提交的几种方式

方式一:Token 机制

  • 客户端请求服务端,获取一个 token,每一次请求都获取到一个全新的 token( token 会有一个超时时间),将 token 存入 redis 中,然后将 token 返回给客户端。
  • 客户端将来携带刚刚返回的 token 去请求一个接口。
  • 服务端收到请求后,分为两种情况:
    • 如果 token 在 redis 中,直接删除该 token,然后继续处理业务请求。
    • 如果 token 不在 redis 中,说明 token 过期或者当前业务已经执行过了,那么此时就不执行业务逻辑。
      特点:实现简单,但是多了一个获取 token 的过程。

方式二:去重表(主要是利用 MySQL 的唯一索引机制来实现的)

  • 客户端请求服务端,服务端将这次的请求信息(请求地址、参数等)存入到一个 MySQL 去重表中,这个去重表要根据这次请求的某个特殊字段建立唯一索引或者主键索引。
  • 判断是否插入成功:
    • 成功:继续完成业务功能。
    • 失败:表示业务已经执行过了,这次就不执行业务了。
      问题:MySQL 的容错性会影响业务、高并发环境可能效率低。

方式三:Redis 的 setnx

  • 客户端请求服务端,服务端将能代表本次请求唯一性的业务字段,通过 setnx 的方式存入 redis,并设置超时时间。
  • 判断 setnx 是否成功:
    • 成功:继续处理业务。
    • 失败:表示业务已经执行过了。

方式四:设置状态字段

  • 给要处理的数据设置一个状态字段。

方式五:锁机制

  • 乐观锁:数据库中增加版本号字段,每次更新都根据版本号来判断。
  • 更新之前先去查询要更新记录的版本号,第二步更新的时候,将版本号也作为查询条件。
select version from xxx where id = xxx;
update xxx set xxx=xxx where xxx=xxx and version=xxx;
  • 悲观锁, 假设每一次拿数据都会被修改,所以直接上排他锁就行了。
start;
select * from xxx where xxx for update;
update xxx
commit;

方式六:自定义注解

  • 将当前请求的地址参数缓存起来,下次再来一个请求时,去判断和缓存中的请求是否完全一样,一样的话并且小于规定的时间间隔,则认为是重复提交。

二、自定义注解方式的实现

1. 准备工作,解决请求参数为JSON时,采用IO流读取,只能请求一次的问题

2. 封住一个RedisCache简化使用

@Component
public class RedisCache {
    @Autowired
    RedisTemplate redisTemplate;

    public <T> void setCacheObject(final String key, final T value, Integer timeout, final TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    public <T> T getCacheObject(final String key) {
        ValueOperations<String,T> valueOperations = redisTemplate.opsForValue();
        return valueOperations.get(key);
    }
}

3. 完善RepeatSubmitInterceptor拦截器,解析注解,判断是否重复提交

详细步骤见代码注释:

@Component
public class RepeatSubmitInterceptor implements HandlerInterceptor {

    public static final String REPEAT_PARAMS = "repeat_params";
    public static final String REPEAT_TIME = "repeat_time";
    public static final String REPEAT_SUBMIT_KEY = "repeat_submit_key";
    public static final String HEADER = "Authorization";

    @Autowired
    RedisCache redisCache;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //所以的controller方法都会被封装成HandlerMethod
        if (handler instanceof HandlerMethod){
            //分析注解
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            RepeatSubmit repeatSubmit = method.getAnnotation(RepeatSubmit.class);
            //如果注解存在&&请求重复
            if (repeatSubmit != null){
                if (isRepeatSubmit(request,repeatSubmit)){
                    //拦截返回错误信息
                    HashMap<String, Object> map = new HashMap<>();
                    map.put("status",500);
                    map.put("message",repeatSubmit.message());
                    response.setContentType("application/json;charset=utf-8");
                    response.getWriter().write(new ObjectMapper().writeValueAsString(map));
                    return false;
                }
            }
        }
        return true;
    }

    private boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit repeatSubmit) {
        //获取请求参数字符串
        String nowParams = "";
        //RepeatableReadRequestWrapper 说明是JSON格式
        if (request instanceof RepeatableReadRequestWrapper){
            try {
                nowParams = ((RepeatableReadRequestWrapper) request).getReader().readLine();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //否则说明参数是key-value格式
        if (StringUtils.isEmpty(nowParams)){
            try {
                nowParams = new ObjectMapper().writeValueAsString(request.getParameterMap());
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }

        //包装参数和当前时间
        HashMap<String, Object> nowDataMap = new HashMap<>();
        nowDataMap.put(REPEAT_PARAMS,nowParams);
        nowDataMap.put(REPEAT_TIME,System.currentTimeMillis());

        //获取请求信息,组装key
        String requestURI = request.getRequestURI();
        String header = request.getHeader(HEADER);
        String cacheKey = REPEAT_SUBMIT_KEY + requestURI + header.replace("Bearer ","");

        //根据key查找redis
        Object cacheObject = redisCache.getCacheObject(cacheKey);

        if (cacheObject != null){
            //这里说明不是第一次,判断是否为重复请求(参数、时间)
            Map<String, Object> cacheMap = (Map<String, Object>) cacheObject;
            if (compareParams(cacheMap, nowDataMap) && compareTime(cacheMap, nowDataMap, repeatSubmit.interval())){
                return true;
            }
        }

        //到这里说明是第一次访问
        redisCache.setCacheObject(cacheKey,nowDataMap,repeatSubmit.interval(), TimeUnit.MILLISECONDS);
        return false;
    }

    private boolean compareTime(Map<String, Object> cacheMap, HashMap<String, Object> nowDataMap, int interval) {
        Long nowTime = (Long) nowDataMap.get(REPEAT_TIME);
        Long cacheTime = (Long) cacheMap.get(REPEAT_TIME);
        if (nowTime - cacheTime < interval) {
            return true;
        }
        return false;
    }

    private boolean compareParams(Map<String, Object> cacheMap, HashMap<String, Object> nowDataMap) {
        String cacheParams = (String) cacheMap.get(REPEAT_PARAMS);
        String nowParams = (String) nowDataMap.get(REPEAT_PARAMS);
        return nowParams.equals(cacheParams);
    }

}

4. 测试注解

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
到这里,一个自定义注解方式的防重就实现完了,点击跳转源码仓库地址。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Anton丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值