Springboot AOP接口防刷、防重复提交

发布于:2024-04-24 ⋅ 阅读:(26) ⋅ 点赞:(0)

Java利用注解、Redis做防重复提交和限流
使用场景
用户网络慢,电脑卡,一直点击保存,修改按钮无返回信息,会导致多个请求去保存、修改
开放接口、或加密接口频繁访问,会导致程序压力大,可能被他人写脚本一直请求接口
解决方案
前端js提交后禁止按钮,返回结果后解禁(前端不严谨,点击速度快,也可重复提交)
在java中添加自定义防重复提交注解 @RepeatSubmit ,利用AOP切入,其次用Redis临时存入唯一信息。开放接口把请求的IP、请求路径、请求的电脑User-Agent拼接为唯一key,未开发接口按照使用场景,组装为唯一key
等等…             

首当其冲肯定是先引入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

package cn.tpson.parking.module.base.params.annotate;


import java.lang.annotation.*;

/**
 * 自定义防重提交注解
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
    /**
     * 默认防重提交是方法参数
     */
    Type limitType() default Type.PARAM;

    /**
     * 加锁过期时间,默认是 5s
     * 比如通过redis的key来校验是否重复提交,
     * 这个5s就是设置的key的过期时间
     */
    long lockTime() default 5;

    /**
     * 防重提交,支持两种,一个方法参数,一个是令牌
     */
    enum  Type {PARAM,TOKEN }

}

定义AOP切面类:RepeatSubmitAspect,现在定义两种重复提交或限流,

第一种:获取用户电脑信息、获取请求IP地址、获取请求Url 。

第二种:获取请求里的token、获取请求IP地址。如不符合场景,可在repeatSubmit环绕通知方法中重写。注(方法中使用获取IP工具类、常量类,CommonConstant为常量,可直接去创建)

package cn.tpson.parking.module.base.aspect;


import cn.tpson.parking.framework.common.util.IpKit;
import cn.tpson.parking.module.base.params.annotate.RepeatSubmit;
import cn.xtool.core.rest.Result;
import lombok.RequiredArgsConstructor;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;

@Aspect
@RequiredArgsConstructor
@Component
public class RepeatSubmitAspect {
    protected static final Logger logger = LoggerFactory.getLogger(RepeatSubmitAspect.class);
    private final RedisTemplate<String, Object> redisTemplate;

    @Pointcut("@annotation(repeatSubmit)")
    public void pointNoRepeatSubmit(RepeatSubmit repeatSubmit) {
    }

    /**
     * 利用Redis实现的防重复提交拦截器。
     *
     * @param joinPoint    切面连接点,表示被拦截的方法。
     * @param repeatSubmit 重复提交注解对象,包含锁的时间等配置。
     * @return 返回方法执行结果,若重复提交则返回失败信息。
     * @throws Throwable 如果方法执行过程中出现异常,则抛出。
     */
    @Around(value = "pointNoRepeatSubmit(repeatSubmit)", argNames = "joinPoint,repeatSubmit")
    public Object repeatSubmit(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
        logger.info("-----------防止重复提交开始----------");
        // 获取当前请求的属性
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            logger.error("ServletRequestAttributes is null.");
            return Result.fail("系统异常");
        }
        // 从请求中获取必要的信息来构建唯一键
        HttpServletRequest request = attributes.getRequest();
        String key = buildKey(request, repeatSubmit);

        // 尝试加锁,防止重复提交
        if (!tryLock(key, repeatSubmit.lockTime())) {
            String repeatMsg = "请勿重复提交或者操作过于频繁! 请在" + repeatSubmit.lockTime() + "秒后重试";
            logger.info(repeatMsg);
            return Result.fail(repeatMsg);
        }

        try {
            logger.debug("通过,执行下一步");
            // 执行被拦截的方法
            Object o = joinPoint.proceed();
            logger.info("----------防止重复提交设置结束----------");
            return o;
        } catch (Exception e) {
            logger.error("方法执行异常", e);
            throw e;
        } finally {
            // 无论方法执行结果如何,最后都释放锁
            redisTemplate.delete(key);
        }
    }

    /**
     * 尝试对给定的键加锁。
     *
     * @param key      键名,用于Redis中标识一个锁。
     * @param lockTime 锁定的时间,单位秒。
     * @return 如果加锁成功返回true,否则返回false。
     */
    private boolean tryLock(String key, Long lockTime) {
        Boolean locked = redisTemplate.opsForValue().setIfAbsent(key, key);
        if (Boolean.TRUE.equals(locked)) {
            redisTemplate.expire(key, lockTime, TimeUnit.SECONDS);
            return true;
        }
        return false;
    }

    /**
     * 根据请求信息和注解配置构建唯一键。
     *
     * @param request      HttpServletRequest对象,用于获取请求信息。
     * @param repeatSubmit 重复提交注解对象,配置限制类型等。
     * @return 返回构建好的唯一键字符串。
     */
    private String buildKey(HttpServletRequest request, RepeatSubmit repeatSubmit) {
        StringBuilder key = new StringBuilder();
        String limitType = repeatSubmit.limitType().name();
        // 根据限制类型构建键名
        if (limitType.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
            key.append(IpKit.getIpAdrress(request)).append("-").append(request.getRequestURI());
        }
        logger.info("防止重复提交Key:{}", key);
        return key.toString();
    }
}

其中ResultAPI为统一返回结果

使用示例: 

    @PutMapping("/sendLoginCode")
    @ApiOperation(value = "发送登录验证码")
    @RepeatSubmit(lockTime = 60L, limitType = RepeatSubmit.Type.PARAM)
    public Result<Boolean> sendLoginCode(@ApiIgnore HttpServletRequest request, @Valid @RequestBody PhoneLoginSendCodeDTO dto) {
        validCaptcha(request, dto.getCode());
        return Result.ok(userService.sendLoginCode(dto, PlatformTypeEnum.CUSTOMER));
    }

第一次访问结果

 第二次访问