如何防止接口被重复提交?
接口幂等性就是用户对同一操作发起的一次请求或多次请求的结果是一致的,不会因为对此提交而重复执行或出现其他问题。 例:在支付的时候,如果用户以为没支付成功(实际成功了),再次点击按钮导致被扣了两次钱,这是无法接收的问题,所以这个给问题是一定要解决的。本文使用的是AOP切片来解决这个问题。
再后端通过自定义注解,将这个注解作为切点,再在需要防幂等接口上添加注解,在执行方法之前在切片当中进行判断是否是重复提交。这样减少了和业务的耦合。具体的是在切点当中获取用户的token、user_id、url用于作为redis的唯一key,因为redis有自动过期机制,所以只要给这个key设置过期时间,就可以让这个用户在这个key还没过期之前无法重复调用这个接口。
具体实现:
- 导入这个aop依赖和Redis依赖,注意还要有一些springboot基础依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 编写注解。
@Target(ElementType.METHOD) // 注解只能用于方法
@Retention(RetentionPolicy.RUNTIME) // 修饰注解的生命周期
@Documented
public @interface RepeatSubmit {
/**
* 防重复操作过期时间,默认1s
*/
long expireTime() default 1;
}
- 编写切片,将那个注解变为切点
@Slf4j
@Component
@Aspect
public class RepeatSubmitAspect {
@Autowired
private RedisTemplate redisTemplate;
/**
* 定义切点
*/
@Pointcut("@annotation(com.example.demo.apo.RepeatSubmit)")//这里填写自己那个注解的路径
public void repeatSubmit() {}
@Around("repeatSubmit()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 获取防重复提交注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
// 获取token当做key,这里是新后端项目获取不到哈,先写死
// String token = request.getHeader("Authorization");
String tokenKey = "hhhhhhh,nihao";
if (StringUtils.isBlank(tokenKey)) {
throw new RuntimeException("token不存在,请登录!");
}
String url = request.getRequestURI();
/**
* 通过前缀 + url + token 来生成redis上的 key
* 可以在加上用户id,这里没办法获取,大家可以在项目中加上
*/
String redisKey = "repeat_submit_key:"
.concat(url)
.concat(tokenKey);
log.info("==========redisKey ====== {}",redisKey);
if (!redisTemplate.hasKey(redisKey)) {
redisTemplate.opsForValue().set(redisKey, redisKey, annotation.expireTime(), TimeUnit.SECONDS);
try {
//正常执行方法并返回
return joinPoint.proceed();
} catch (Throwable throwable) {
redisTemplate.delete(redisKey);
throw new Throwable(throwable);
}
} else {
// 抛出异常
throw new Throwable("请勿重复提交");
}
}
}
- 编写接口
- 这一步自己可以随便写一个接口,只要给接口加上切点(也就是这里的@RepeatSubmit注解),别用我这里的代码,
@RepeatSubmit(expireTime = 10)//为了方便展示不能重复提交的功能这里将过期时间设置为10s
@GetMapping("/open/logins")
public ResultUtil logins(UserParam userParam){
String login = iUserService.login(userParam);
return ResultUtil.success(login);
}
测试:
第一次:
第二次
10s后: