spring aop 、Redis实现拦截重复操作

一、问题:项目中有一些重复操作的情况,比如:

  1.从场景有用户快速点击提交按钮,或者postMan测试时快速点击

  2.从业务上来说,用户注册、用户下单等

  3.黑客攻击

二、解决办法

  1、使用springAop、Redis

  2、代码

/**
 * 2020/7/22 9:59 AM
 *
 * @author shoo
 * @describe 校验重复操作 (aop实现)
 */
@Aspect
@Component
public class ParaLogAspect {

    private static final Logger logger = LoggerFactory.getLogger(ParaLogAspect.class);
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    //定义一个切点,要切的类、方法
    @Pointcut("execution(* com.meritdata.cloud.middleplatform.dataservice.account.integral.controller.*.*(..))" +
            "||execution(* com.meritdata.cloud.middleplatform.dataservice.account.platformBase.controller.*.*(..))" +
            "||execution(* com.meritdata.cloud.middleplatform.dataservice.account.storeConsume.controller.*.*(..))" +
            "||execution(* com.meritdata.cloud.middleplatform.dataservice.account.vipcard.controller.*.*(..))" +
            "||execution(* com.meritdata.cloud.middleplatform.dataservice.shell.controller.*.*(..))")
    public void authRepeat(){

    }

    @Around("authRepeat()")
    public Object authRepeat(ProceedingJoinPoint joinPoint) throws Throwable{

        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        StringBuffer body = new StringBuffer();
        Object[] arguments = joinPoint.getArgs();
        //获取方法的参数
        //注意这里只取了第一个参数,如果想兼容多个参数的方法请自行处理
        if(arguments.length!=0){
            try {
                Map<String, Object> params = params = (Map<String, Object>)arguments[0];
                for (String key:params.keySet()
                ) {
                    body.append(key).append("=").append(params.get(key)).append("&");
                }

            }catch (Exception ex){
                logger.info("=====方法接收参数[{}]",arguments[0].toString());
            }
        }
        // key:请求者IP+请求URL+参数
        String key = request.getRemoteAddr() + ";" + request.getRequestURL().toString() + "?" + body.toString();
        logger.info("====key[{}]",key.substring(0,key.length()-1));

        Object obj = null;
        Object[] args = joinPoint.getArgs();
        //重复提交校验
        if(!authRepeat(key)){
            logger.info("重复提交,key[{}]",key);
            return MapResult.build("请勿频繁操作",false);
        }
        //不是重复提交则继续主进程
        try {
            obj = joinPoint.proceed(args);
        } catch (Throwable e) {
            logger.error("重复操作校验环绕通知出错", e);
        }
        return obj;
    }

    //重复提交校验
    // Redis的increment方法:把key的值加上指定数值,如果key不存在则默认创建,该操作是单线程的
    private  boolean authRepeat(String key){
        
        long repeat = redisTemplate.opsForValue().increment(key,1);
        logger.info("repeat:[{}]",repeat);
        if(repeat>1){
            return false;
        }
        redisTemplate.expire(key,1, TimeUnit.SECONDS);
        return true;
    }
}

  3.说明

   a、首先用springAop切入需要校验的类或者方法,这里用的是环绕通知(around),如果一秒内操作次数超过一次则返回错误提示请勿频繁操作

   b、校验规则是 请求者IP+请求URL+方法参数

   c、Redis的increment是单线程的原子操作

三、测试

  用locust启动十个用户同时访问用户注册接口,数据库中只注册成功了一个数据,剩余的都提示请勿频繁操作

stay hungry stay foolish!
原文地址:https://www.cnblogs.com/shog808/p/14078787.html