日常开发项目中,经常会要求部分接口做防刷、防重复提交的拦截,比如:获取验证码(前端亦可加入倒计时实现,60秒不能再次获取),下单接口 (网络原因或其他什么原因,有时候点击下单支付,感觉没反应 连点了n次, 导致几秒内相同一个东西支付下单了多笔,被银行风控的情况)等等,所以有些接口我们有必要做防重复提交的拦截,今天就来简单聊聊这个话题.
首当其冲肯定是先引入AOP依赖,maven为例 pom.xml
<!-- aop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- JSON依赖 -->
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
有了AOP的支持 接下来我们进行自定义注解 NoRepeatSubmit
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)//作用于方法上
@Retention(RetentionPolicy.RUNTIME)//运行时
public @interface NoRepeatSubmit {
/**
* 设置请求锁定时间 默认5秒
*/
int lockTime() default 5000;
/**
* 当发生重复提交时候默认返回的错误信息
*/
String errMsg() default "重复提交,请 second 秒后重试";
}
RepeatSubmitAspect切面
package com.karo.unicorn.aspect;
import com.karo.unicorn.annotation.NoRepeatSubmit;
import com.karo.unicorn.common.result.ResultAPI;
import com.karo.unicorn.utils.ObjectUtil;
import com.karo.unicorn.utils.http.HttpContextUtils;
import com.karo.unicorn.utils.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
/**
* @ClassName RepeatSubmitAspect
* @Description TODO :
* @Author :Panguaxe
* @Date 2020-05-26 16:08
* @Version V1.0
*/
@Slf4j
@Aspect
@Component
public class RepeatSubmitAspect {
@Autowired
private RedisTemplate redisTemplate;
@Pointcut("@annotation(noRepeatSubmit)")
public void pointcut(NoRepeatSubmit noRepeatSubmit) {
}
@Around("@annotation(noRepeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) throws Throwable {
log.warn("AOP防重复提交设置的加锁时间:{}", noRepeatSubmit.lockTime());
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
// 请求的方法参数值 POST的JSON请求request.getParameter()是获取不到值的
Object[] args = joinPoint.getArgs();
JSONObject requestParams = ObjectUtil.isNotBlank(args) ? JSONObject.fromObject(args[0]) : null;
log.warn("AOP防重复提交[" + request.getRequestURI() + "]接口的请求参数:{}",requestParams);
// 此处可以用token或者JSessionId TODO 或者使用用户ID作为标识
//String token = request.getHeader("Authorization");//根据自己业务替换为你的唯一标识
//String token = request.getHeader("token");//根据自己业务替换为你的唯一标识
String key = JSONUtil.getValNetsf(requestParams, "userId") + request.getServletPath();
log.warn("AOP防重复提交,加锁Key[" + key + "]:{}",requestParams);
boolean isSuccess = tryLock(key, noRepeatSubmit.lockTime());
log.warn("AOP防重复提交,是否放行:{}",isSuccess ? "放行" : "拦截重复提交");
if (!isSuccess) {// 获取锁失败,认为是重复提交的请求
return new ResultAPI().error(noRepeatSubmit.errMsg().replace("second",String.valueOf(noRepeatSubmit.lockTime()/1000)),"");
}
// 获取锁成功, 执行进程
Object result;
try {
result = joinPoint.proceed();
} finally {
redisTemplate.delete(key);// 解锁
}
return result;
}
/**
* @MethodName: tryLock
* @Param: [key --- key值, lockSeconds 时长]
* @Return: boolean 是否获取到
* @Author: Panguaxe
* @Date: 2020-05-26 16:13
* @Description: TODO 最终加强分布式锁
*/
private boolean tryLock(String key, int lockSeconds) {
//lambda表达式
return (Boolean) redisTemplate.execute((RedisCallback) connection -> {
long expireAt = System.currentTimeMillis() + lockSeconds + 1;
log.warn("失效时间:{}", expireAt);
Boolean acquire = connection.setNX(key.getBytes(), String.valueOf(expireAt).getBytes());
if (acquire) {
return true;
}
byte[] value = connection.get(key.getBytes());
if (Objects.nonNull(value) && value.length > 0) {
if (Long.parseLong(new String(value)) < System.currentTimeMillis()) {
// 如果锁已经过期
byte[] oldValue = connection.getSet(key.getBytes(),String.valueOf(System.currentTimeMillis() + lockSeconds + 1).getBytes());
// 防止死锁
return Long.parseLong(new String(oldValue)) < System.currentTimeMillis();
}
}
return false;
});
}
}
其中ResultAPI为统一返回结果
使用示例:
//@Validated({Update.class,Create.class}) //也可以注释掉 此注解为个人测试自定义参数校验注解用
@NoRepeatSubmit(lockTime = 3000)//3000即3秒内不能重复提交
@ExceptionHandler(value = Exception.class)//此处可注释掉 统一异常处理
@PostMapping("noRepeatSubmit")
public APIResult noRepeatSubmit(@RequestBody @Validated({Update.class,Create.class}) UserInfo userInfo){
log.warn("请求参数:{}", JSON.toJSONString(userInfo));
APIResult result = new APIResult();
try {
result.success(userInfo);//因为仅测试防重复提交拦截 不做业务处理 直接请求参数返回 仅验证3秒内同一用户 同一接口不能重复提交
}catch (Exception e){
result.systemError(e.getMessage());
}
return result;
}