哪些因素会引起重复提交?
实际上,造成这种情况的场景不少:
1.网络波动:因为网络波动,造成重复请求。
2.用户的重复性操作:用户误操作,或者因为接口响应慢,而导致用户耐性消失,有意多次触发请求。
3.重试机制:这种情况,经常出现在调用三方接口的时候。对可能出现的异常情况抛弃,然后进行固定次数的接口重复调用,直到接口返回正常结果。
4.黑客或恶意用户使用postman等http工具重复恶意提交表单。
实现思路:
1. 自定义一个注解,添加了该注解的接口可以防止重复提交。
2. 定义切面类,对使用了自定义注解的接口做代理。
3. 使用IP地址(ipAddr)+请求路径(path)作为key,生成一个UUID值作为value值,存入redis,设置有效期,当有请求调用接口时,到redis中查找相应的key,如果能找到,则说明重复提交,如果找不到,则存入Redis。
1.1定义一个注解
/**
* 重复点击的切面
*
* @author tom
* @date 2023/3/29
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
/**
* 提示消息
*/
String message() default "";
/**
* 锁过期的时间
*/
int seconds() default 2;
}
1.2 定义一个reids接口IRedisService
public interface IRedisService {
boolean expire(String key, long timeout);
boolean expire(String key, long timeout, TimeUnit unit);
Long lPushAll(String key, Long time, Object... values);
List<Object> getRangList(String key, long start, long end);
}
1.3定义Redis实现类IRedisServiceImpl
@Service
@Slf4j
public class IRedisServiceImpl implements IRedisService {
@Autowired
private RedisTemplate redisTemplate;
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
@Override
public boolean expire(final String key, final long timeout) {
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
@Override
public boolean expire(final String key, final long timeout, final TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
@Override
public Long lPushAll(String key, Long time, Object... values) {
Long count = redisTemplate.opsForList().rightPushAll(key, values);
expire(key, time);
return count;
}
@Override
public List<Object> getRangList(String key, long start, long end) {
try {
List<Object> result = redisTemplate.opsForList().range(key, start, end);
return result;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
1.4切面类NoRepeatSubmitAspect
/**
* 防止重复点击接口
* * @author tom
* * @date 2023/3/29
*/
@Aspect
@Component
public class NoRepeatSubmitAspect {
private static final Logger logger = LoggerFactory.getLogger(NoRepeatSubmitAspect.class);
@Autowired
private IRedisService redisService;
/**
* 横切点
*/
@Pointcut("@annotation(noRepeatSubmit)")
public void repeatPoint(NoRepeatSubmit noRepeatSubmit) {
}
/**
* 接收请求,并记录数据
*/
@Around(value = "repeatPoint(noRepeatSubmit)")
public Object doBefore(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//request不能为null
if (ObjectUtils.isEmpty(request)){
throw new CustomException("request不能为null");
}
// 此处可以用token或者JSessionId
String token = GetIp.getIpAddr(request);
String path = request.getServletPath();
String key = getKey(token, path);
String clientId = getClientId();
List<Object> rangList = redisService.getRangList(key, 0, -1);
// 获取注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
NoRepeatSubmit annotation = method.getAnnotation(NoRepeatSubmit.class);
long timeout = annotation.seconds();
String message = annotation.message();
boolean isSuccess = false;
if (rangList.size()==0 || rangList == null) {
isSuccess = redisService.lPushAll(key, timeout, clientId)>0;
}
if (!isSuccess) {
// 获取锁失败,认为是重复提交的请求
redisService.lPushAll(key, timeout, clientId);
throw new CustomException(message);
}
try {
Object proceed = joinPoint.proceed();
return proceed;
} catch (Throwable throwable) {
logger.error("运行业务代码出错", throwable);
throw new RuntimeException(throwable.getMessage());
}
}
private String getKey(String token, String path) {
return token + path;
}
private String getClientId() {
return UUID.randomUUID().toString();
}
}
1.5注解实现控制接口防重
/**
* 接口防重测试
*
* @param
* @return
*/
@GetMapping("/interfaceTesting")
@ApiOperation("接口防重测试")
@NoRepeatSubmit(message = "您刚已提交过,请勿重复提交", seconds = 5)
public ResponseResult<InterfaceTestingDto> interfaceTesting() {
log.info("接口防重测试");
return interfaceService.InterfaceTesting();
}
1.6压测工具 fiddler
1.7测试