前言:

重复提交是指页面按钮提交时的误操作点击多次,或者用户估计进行快速点击多次按钮行为,破坏系统的稳定性,进而增加一页安全限制对系统进行保护

本次系统实现的方案:

  1. 单体应用
  2. 不对接redis
  3. spring-boot项目
  4. 使用内存缓存

代码实现

才有注解+切面的方式进行拦截校验,之所以这样做而不是用intceptor是因为这样自由度更高,因为我们基本都是对有写操作的接口进行保护的,而读接口是不用校验的

1、定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {

}

2、定义数据模型

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UrlParamValue {

    private String userAddressId;
    private String methodName;
    private Object[] args;
    private Long time;


    public int hashValue() {
       return Objects.hash(userAddressId, methodName, Arrays.hashCode(args));
    }

}

3、定义切面,实现拦截功能

@Aspect
@Component
@Slf4j
public class NoRepeatSubmitAspect {

    @Value(value = "${noRepeatSubmit.expireTime:500}")
    private Long expireTime;
    // 内存缓存,基于Guava Cache
    private Cache<Integer, UrlParamValue> PARAM_REQUEST_CACHE = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(2, TimeUnit.SECONDS)
            .build();

    /**
     * 横切点
     */
    @Pointcut("@annotation(noRepeatSubmit)")
    public void repeatPoint(NoRepeatSubmit noRepeatSubmit) {
    }

    /**
     * 接收请求,并记录数据
     */
    @Around(value = "repeatPoint(noRepeatSubmit)")
    public Object doBefore(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) {
        Object[] args = joinPoint.getArgs();
        String methodName = joinPoint.getSignature().toShortString();
        // 获取用户信息,使用自己的方式进行替换
        User user = AuthUtils.instance().getUser();
	// 目前根据用户、类方法、参数 hash 确定是否唯一
        UrlParamValue paramValue = new UrlParamValue(user.getAddressId(), methodName, args, System.currentTimeMillis());
        int hash = paramValue.hashValue();
        try {
            UrlParamValue cacheParam = PARAM_REQUEST_CACHE.get(hash, () -> paramValue);
            // 判断是否是刚放进去的数据
            if (!paramValue.equals(cacheParam)) {
 		// 判断间隔时间
                if (paramValue.getTime() - cacheParam.getTime() < expireTime) {
                    log.warn("userAddressId={},method={}访问过于频繁", paramValue.getUserAddressId(), paramValue.getMethodName());
                    return BaseResponse.error(ResponseCode.OPERATE_REPEATEDLY);
                }
            }
        } catch (ExecutionException e) {
            log.error("用户拦截切面缓存执行异常", e);
        }
        PARAM_REQUEST_CACHE.put(hash, paramValue);
        try {
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            if (throwable instanceof BizException) {
                return BaseResponse.error(((BizException) throwable).getErrorInfo());
            }
            log.error("运行业务代码出错", throwable);
            return BaseResponse.error(ResponseCode.SYSTEM_ERROR);
        }
    }

}

4、使用


    @PostMapping("challenge")
    // 增加注解即可
    @NoRepeatSubmit
    public BaseResponse<BattleChallengeResponse> challenge(@Validated @RequestBody BattlePKRequest request) {
        xxxxx
	xxxxx
    }