防重复提交
防重复提交
作者:kaoli
防重复提交,幂等性提交,这个有的直接在前端做,不过我觉得这类的放在后端实现更安全。
网上有好多这类的实现,这里也记录一下我的实现方式,在生产上也跑了一段时间,目前没啥问题。。。
随大流,也是通过 redis set key nx ex expire 实现。。。
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author : kaoli
* @description : 防重复提交注解
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 间隔时间(s),小于此时间视为重复提交
*/
int interval() default 2;
/**
* 提示消息
*/
String message() default "请勿重复提交";
/**
* 排除字段,使用的是 redis set key nx ex expire
* 其中key由(uri | token + : + param -> [SHA1])构成,默写情况下期望排除部属性,可在该列填写排除的属性名。
* 场景,例如 传的参数中有当前时间,两次请求间时间内相差毫秒级,但是期望这两次请求为重复请求,则排除时间参数。一般用不到。。。
*/
String[] exclude() default {};
}
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SimplePropertyPreFilter;
import com.pingpongx.common.exception.DataConflictException;
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.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DigestUtils;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
/**
* @author : kaoli
* @description : 防重复提交切面
*/
@Aspect
@Component
public class RepeatSubmitAspect {
private RedisTemplate<String, String> redisTemplate;
/**
* 防重提交 redis key
*/
public static final String REPEAT_SUBMIT_KEY = "repeat_submit:";
/**
* 令牌
*/
public static final String TOKEN = "Authorization";
public RepeatSubmitAspect(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 定义切点
*/
@Pointcut("@annotation(com.*.*.*.*.RepeatSubmit)")
public void preventDuplication() {
}
/**
* 如果key不存在,set key and expire key
* set key value [EX seconds] [PX milliseconds] [NX|XX]
* EX seconds:设置失效时长,单位秒
* PX milliseconds:设置失效时长,单位毫秒
* NX:key不存在时设置value,成功返回OK,失败返回(nil)
* XX:key存在时设置value,成功返回OK,失败返回(nil)
*
* @param key
* @param value
* @param expire
* @return
*/
public Boolean setIfAbsent(String key, String value, int expire) {
// NOTE:直接SETNX不支持带过期时间,所以设置+过期不是原子操作,极端情况下可能设置了就不过期了,后面相同请求可能会误以为需要去重,所以这里使用底层API,保证SETNX+过期时间是原子操作。。。测试用的环境比较老,近几年redisTemplate已有setIfAbsent方法,可以直接设置过期时间 redisTemplate.opsFoeValue().setIfAbsent()
return redisTemplate.execute((RedisCallback<Boolean>) connection ->
connection.set(key.getBytes(), value.getBytes(), Expiration.seconds(expire),
RedisStringCommands.SetOption.SET_IF_ABSENT));
}
/**
* 校验重复提交
*
* @param joinPoint
* @return
*/
@Around("preventDuplication()")
public Object before(ProceedingJoinPoint joinPoint)throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Assert.notNull(request, "request cannot be null.");
//获取执行方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//获取防重复提交注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
// 获取token以及方法标记,生成redisKey和redisValue
String submitKey = request.getHeader(TOKEN);
// 若无登录用户,则获取uri作为key的一部分
submitKey = submitKey == null ? request.getRequestURI(): submitKey.replaceAll("bearer ", "");
String redisKey = REPEAT_SUBMIT_KEY
.concat(submitKey).concat(":")
.concat(getMethodSign(annotation.exclude(), method, joinPoint.getArgs()));
String redisValue = annotation.message() + "|" + annotation.interval();
if (setIfAbsent(redisKey, redisValue, annotation.interval())) {
try {
return joinPoint.proceed();
} catch (Throwable e){
//确保方法执行异常实时释放限时标记
redisTemplate.delete(redisKey);
throw e;
}
}
throw new DataConflictException(annotation.message());
}
/**
* 生成方法标记:采用数字签名算法SHA1对方法签名字符串加签
*
* @param exclude
* @param method
* @param args
* @return
*/
private String getMethodSign(String[] exclude, Method method, Object... args) {
StringBuilder sb = new StringBuilder(method.toString());
for (Object arg : args) {
sb.append(toString(arg, exclude));
}
return DigestUtils.sha1DigestAsHex(sb.toString());
}
private String toString(Object arg, String[] exclude) {
if (Objects.isNull(arg)) {
return "null";
}
if (arg instanceof Number) {
return arg.toString();
}
if (exclude != null && exclude.length > 0) {
SimplePropertyPreFilter excludeFilter = new SimplePropertyPreFilter();
// 注意:如果类包含子对象,下级同字段名的也会被排除掉
excludeFilter.getExcludes().addAll(Arrays.asList(exclude));
return JSONObject.toJSONString(arg, excludeFilter);
}
return JSONObject.toJSONString(arg);
}
}
// 切面不失效的地方使用都行
/**
* 新增
*
* @param dto
* @return
*/
@PostMapping
@RepeatSubmit(interval = 1, message = "kaoli 😝", exclude = {"dealTime"})
public ApiResponse submitApply(List<MultipartFile> files, @Validated ApplyInfo dto) {
// ......
return null;
}
本文来自博客园,作者:kaoli-烤梨,转载请注明原文链接:https://www.cnblogs.com/kaoli/p/16210446.html

浙公网安备 33010602011771号