禁止重复提交
1.为什么要禁止重复提交?
在我们平时开发的过程中,有很多用户点击提交按钮提交表单或者说用户主动提交某些信息的情景。正常情况下,我们后台正常接收前台提交的内容,然后再进行增删改查等操作。但是,我们都说不能已常理去考虑用户的使用情况。一旦前台提交内容后,因为网络波动或者后台逻辑处理较慢,而前台又没有做禁止点击提交按钮或者等待页面,难免出现用户疯狂点击提交按钮的情况。这种情况就很有可能导致用户的数据多次提交、入库,产生脏数据、冗余数据等情况。
上述只是可能会出现重复提交或者请求的一种情况,实际上,会造成这种情况的场景不少:
- 网络波动:
 因为网络波动,造成重复请求
- 用户的重复性操作:
 用户误操作,或者因为接口响应慢,而导致用户耐性消失,有意多次触发请求
- 重试机制:
 这种情况,经常出现在调用三方接口的时候。对可能出现的异常情况抛弃,然后进行固定次数的接口重复调用,直到接口返回正常结果。
- 分布式消息消费:
 任务发布后,使用分布式消息服务来进行消费(这个我还没有碰到过)
总而言之,禁止重复提交使我们保证数据准确性及安全性的必要操作。
2.springBoot+redis禁止重复提交
首先明确一下思路:
- 这里验证的是: 同一客户端2s内请求同样的url,即视为重复提交
- 在请求进入业务方法之前,进入切面。已sessionId+url作为key,在redis中查询,看是否存在。存在即为重复提交。
- 如果是重复提交则抛出异常,交由controllerAdvice处理。如果不是,则正常处理业务逻辑。
首先定义一个注解,用来表示那些方法需要实现接口幂等性。
/**
 * @ClassName ForbidRepeatCommit
 * @Description 禁止重复提交注解
 * @Author
 * @Version V1.0
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ForbidRepeatCommit {
    String value() default "";
}然后我们写一个切面,用来在参数进行业务操作前,验证是否为重复提交。这个切面对标注@ForbidRepeatCommit注解的方法进行切入。
/**
 * @ClassName RepeatCommitAspectAmend
 * @Description 校验重复提交切面  修正版  使用session+url机制
 * @Author
 * @Date 2019-10-23 下午 2:53
 * @Version V1.0
 */
@Aspect
@Slf4j
@Component
public class RepeatCommitAspectAmend {
    @Autowired
    TokenService tokenService;
    /***
     * controler包下的方法 并且被@ForbidRepeatCommit注解标注
     */
    @Pointcut("execution(public * com.xs.controller..*(..)) &&  @annotation(com.xs.annotations.ForbidRepeatCommit)  && !execution(public * com.xs.controller.TokenController.*(..)) ")
    public void verifyRequestToken() {
    }
    /***
     * 校验是否为重复提交
     * @param joinPoint
     * @throws Exception
     */
    @Before("verifyRequestToken()")
    public void execVerify02(JoinPoint joinPoint) throws Exception {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String sessionId = RequestContextHolder.getRequestAttributes().getSessionId();
        HttpServletRequest request = attributes.getRequest();
        String key = sessionId + "-" + request.getServletPath();
        //  判断是否为重复提交 使用session+url机制
        tokenService.checkTokenBySessionAndUrl(key);
    }
}验证是否为重复提交的TokenServiceImpl:
/**
 * @ClassName TokenServiceImpl
 * @Description 表单重复提交检验service
 * @Author
 * @Version V1.0
 */
@Slf4j
@Service
public class TokenServiceImpl implements TokenService {
    @Autowired
    StringRedisTemplate redisTemplate;
    /***
     * 提交的session+url已经存在redis 视为重复提交  
     * @param key
     * @throws Exception
     */
    @Override
    public void checkTokenBySessionAndUrl(String key) throws Exception {
        if(StringUtils.isEmpty(key)){
            log.error(" key为空 ");
            throw new Exception("key为空");
        }
        //如果缓存中存在此key 视为重复提交
        if(redisTemplate.opsForValue().get(key) == null){
            //不存在 放入redis 设置超时时间为2s
            redisTemplate.opsForValue().set(key,key,2, TimeUnit.SECONDS);
        }else{
            log.error(" 重复提交 ");
            throw new Exception("重复提交");
        }
        log.info(" 此请求成功提交表单 ");
    }
}对controller抛出异常进行统一处理的类:
/**
 * @ClassName ControllerAdviceAspect
 * @Description 全局异常处理
 * @Author
 * @Version V1.0
 */
@Slf4j
@RestControllerAdvice
public class ControllerAdviceAspect {
    /****
     * 处理controller抛出的异常
     * @param request
     * @param e
     * @return
     */
    @ExceptionHandler(Exception.class)
    public Object handleException(HttpServletRequest request, Exception e) {
		//对所有异常进行处理 返回固定的响应体
        ResponseMessage responseMessage = new ResponseMessage();
        responseMessage.setData("");
        responseMessage.setStatus("600");
        responseMessage.setMessage(((UndeclaredThrowableException)e).getUndeclaredThrowable().getMessage());
        return responseMessage;
    }
}以上基本是禁止重复提交的全部代码了。
事实上,在之前也看过很多禁止重复提交的文章。包括网上流传比较广的redis+token机制。个人觉得并不能完全禁止重复提交,所以这里就不提这个了,如果大家有兴趣的话,可以自己搜索一下。