Java利用注解+AOP+Redis做防重复提交和限流

Java利用注解、Redis做防重复提交和限流

使用场景

  1. 用户网络慢,电脑卡,一直点击保存,修改按钮无返回信息,会导致多个请求去保存、修改
  2. 开放接口、或加密接口频繁访问,会导致程序压力大,可能被他人写脚本一直请求接口

解决方案

  1. 前端js提交后禁止按钮,返回结果后解禁(前端不严谨,点击速度快,也可重复提交)
  2. 在java中添加自定义防重复提交注解 @RepeatSubmit ,利用AOP切入,其次用Redis临时存入唯一信息。开放接口把请求的IP、请求路径、请求的电脑User-Agent拼接为唯一key,未开发接口按照使用场景,组装为唯一key
  3. 等等…

实现案例

  1. 进入相关依赖 pom.xml
	    <!-- redis缓存依赖  -->
	    <dependency>
	        <groupId>org.springframework.boot</groupId>
	        <artifactId>spring-boot-starter-data-redis</artifactId>
	    </dependency>
	    <!--  spring aop依赖-->
	    <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>
  1. 定义 @RepeatSubmit 注解
package com.base.sbc.config.aspect.annotation;

/**
 * @author lizan
 * @date 2023-03-13 16:12
 */

import java.lang.annotation.*;

/**
 * @author lizan
 * @date 2023-03-13 16:16
 * 自定义防重提交注解
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
    /**
     * 防重提交,支持两种,一个方法参数,一个是令牌
     */
    enum  Type {PARAM,TOKEN }

    /**
     * 默认防重提交是方法参数
     */
    Type limitType() default Type.PARAM;

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

}

  1. 定义AOP切面类:RepeatSubmitAspect,现在定义两种重复提交或限流,一种:获取用户电脑信息、获取请求IP地址、获取请求Url ,第二种:获取请求里的token、获取请求IP地址。如不符合场景,可在repeatSubmit环绕通知方法中重写。注(方法中使用获取IP工具类、常量类,CommonConstant为常量,可直接去创建,获取IP工具类放在尾部)
package com.base.sbc.config.aspect;

import com.base.sbc.config.aspect.annotation.RepeatSubmit;
import com.base.sbc.config.common.ApiResult;
import com.base.sbc.config.constant.CommonConstant;
import com.base.sbc.config.redis.RedisUtils;
import com.base.sbc.config.utils.IpAdrressUtil;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 防止重复提交AOP
 * @author lizan
 * @date 2023-03-13 16:16
 */
@Aspect
@Component
public class RepeatSubmitAspect {
    /** 日志对象 */
    protected Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private RedisUtils redisUtils;

    /**
     *  定义切入点
     */
    @Pointcut("@annotation(repeatSubmit)")
    public void pointNoRepeatSubmit(RepeatSubmit repeatSubmit) {
    }

    /**
     *  环绕通知, 围绕着方法执行
     * @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();
        assert attributes != null;
        HttpServletRequest request = attributes.getRequest();
        // 这里是唯一标识 根据情况而定
        StringBuilder key = new StringBuilder();
        // 防重提交类型
        String limitType = repeatSubmit.limitType().name();
        // 根据防重提交类型处理 默认防重提交是方法参数
        if (limitType.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
            // 获取用户电脑信息
            key.append(request.getHeader(CommonConstant.USER_AGENT));
            // 获取请求IP地址
            key.append(IpAdrressUtil.getIpAdrress(request));
            key.append("-");
            // 获取请求Url
            key.append(request.getRequestURI());
        } else {
            // 获取请求里的token
            String authorization = request.getHeader(CommonConstant.AUTHORIZATION);
            key.append(authorization);
            key.append("-");
            // 获取请求Url
            key.append(request.getRequestURI());
        }
        logger.info("防止重复提交Key:{}",key.toString());
        // 如果缓存中有这个IP地址,URL视为重复提交
        if (!redisUtils.hasKey(key.toString())) {
            logger.info("防止重复提交设置中,{} 秒内不可反复提交",repeatSubmit.lockTime());
            //通过,执行下一步
            Object o = joinPoint.proceed();
            //然后存入redis 并且设置5s倒计时
            redisUtils.set(key.toString(), key.toString(), repeatSubmit.lockTime());
            logger.info("----------防止重复提交设置结束----------");
            //返回结果
            return o;
        }
        String repeatMsg = "请勿重复提交或者操作过于频繁! 请在"  +repeatSubmit.lockTime() + "秒后重试";
        logger.info(repeatMsg);
        return ApiResult.error(repeatMsg,
                CommonConstant.SC_INTERNAL_SERVER_ERROR_500);


    }
}

  1. 获取IP地址工具类 IpAdrressUtil
package com.base.sbc.config.utils;

import javax.servlet.http.HttpServletRequest;
/**
 * 获取IP地址工具类
 * @author lizan
 * @date 2023-03-13 16:16
 */
public class IpAdrressUtil {

    /**
     * 获取Ip地址
     * @param request
     * @return
     */
    public static String getIpAdrress(HttpServletRequest request) {
        String xip = request.getHeader("X-Real-IP");
        String xFor = request.getHeader("X-Forwarded-For");
        if(StringUtils.isNotEmpty(xFor) && !"unKnown".equalsIgnoreCase(xFor)){
            //多次反向代理后会有多个ip值,第一个ip才是真实ip
            int index = xFor.indexOf(",");
            if(index != -1){
                return xFor.substring(0,index);
            }else{
                return xFor;
            }
        }
        xFor = xip;
        if(StringUtils.isNotEmpty(xFor) && !"unKnown".equalsIgnoreCase(xFor)){
            return xFor;
        }
        if (StringUtils.isBlank(xFor) || "unknown".equalsIgnoreCase(xFor)) {
            xFor = request.getHeader("Proxy-Client-IP");
        }
        if (StringUtils.isBlank(xFor) || "unknown".equalsIgnoreCase(xFor)) {
            xFor = request.getHeader("WL-Proxy-Client-IP");
        }
        if (StringUtils.isBlank(xFor) || "unknown".equalsIgnoreCase(xFor)) {
            xFor = request.getHeader("HTTP_CLIENT_IP");
        }
        if (StringUtils.isBlank(xFor) || "unknown".equalsIgnoreCase(xFor)) {
            xFor = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (StringUtils.isBlank(xFor) || "unknown".equalsIgnoreCase(xFor)) {
            xFor = request.getRemoteAddr();
        }
        return xFor;
    }
}

  1. 在访问接口中添加 @RepeatSubmit ,并进行测试
    @GetMapping("/test")
    @RepeatSubmit(lockTime = 3L, limitType = RepeatSubmit.Type.PARAM)
    public ApiResult test(@RequestParam("userCompany") String userCompany,
                                  @RequestParam("id") String id) throws Exception {
        return selectSuccess("访问成功");

    }
  1. 结果
    第一次访问结果在这里插入图片描述
    第二次访问
    在这里插入图片描述
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值