表单重复提交,我认为和幂等还是有一点区别的。
这里要防止的就是表单重复提交,比如前端没做loading而你手速快点了多次,网络重试等。
某些操作不做防护其实还是有问题的,比如项目里面批量生成兑换码,一批生成10000个,手一抖生成了2万个。
思路:
URI+userId+参数 ,做一下md5哈希,作为key,放到redis打标记,value值可以随意,并设置有效期,比如3秒。
统一在aop做处理,自定义注解支持不同接口自定义延迟时间,因为每个业务要求不同。
代码:
注解
import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RepeatSubmit { /** * 延迟多少秒后可以重复提交 * @return */ long delaySecond() default 1; }
Aspect切面
package com.xdf.ucan.web.aop; import cn.hutool.crypto.digest.MD5; import com.alibaba.fastjson.JSONArray; import com.xdf.ucan.service.response.Result; import com.xdf.ucan.service.utils.RedisKeyUtil; import com.xdf.ucan.service.utils.RequestWebUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; /** * @author lihaoyang * @date 2022/2/18 */ @Aspect @Component @Slf4j public class RepeatSubmitAspect { @Autowired private HttpServletRequest request; @Autowired private StringRedisTemplate stringRedisTemplate; @Pointcut("execution(public * com.xdf.ucan.web.controller..*.*(..))") public void controllerPoint() { } @Around("controllerPoint()") public Object doControllerPointAround(ProceedingJoinPoint joinPoint) throws Throwable { Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); //获取注解 RepeatSubmit repeatSubmit = AnnotationUtils.findAnnotation(method, RepeatSubmit.class); if (Objects.isNull(repeatSubmit)) { return joinPoint.proceed(); } //延迟秒数 long delaySecond = repeatSubmit.delaySecond(); //请求参数 Object[] args = joinPoint.getArgs(); String accountId = RequestWebUtil.getAccountId(); String lockKeyParam = request.getRequestURI() + "/" + accountId; if (ArrayUtils.isNotEmpty(args)) { List<Object> argsList = Arrays.asList(args); JSONArray jsonArray = new JSONArray(argsList); lockKeyParam += jsonArray.toJSONString(); } String lockKey = RedisKeyUtil.getRepeatSubmitKey(MD5.create().digestHex(lockKeyParam)); Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, accountId, delaySecond, TimeUnit.SECONDS); if (!isLock) { //请稍后执行 log.info("重复提交,lockKey:{} ", lockKey); return Result.error("重复提交,请"+delaySecond+"秒后重试"); } return joinPoint.proceed(); } }
使用
/** * 生成商品兑换码 * * @param generateReq * @return */ @RepeatSubmit(delaySecond = 5) @PostMapping("generate") public Result generate(@Valid @RequestBody ExchangeCodeGenerateReq generateReq) { generateReq.setAccountId(RequestWebUtil.getAccountId()); log.info("生成兑换码,参数:{}", JSONUtil.toJsonStr(generateReq)); String batchId = ""; try { batchId = exchangeCodeService.batchGenerate(generateReq); } catch (Exception e) { return Result.error("生成失败,请重试"); } return Result.OK(batchId); }
Redis打的标记
SECONDS