开发者

使用Redis控制表单重复提交和控制接口访问频率方式

目录
  • 场景一:控制表单重复提交
    • 背景
    • 实现代码
  • 场景二:控制接口调用频率
    • 背景
    • 实现代码
  • 总结

    场景一:控制表单重复提交

    防重提交有很多方案,从前端的按钮置灰,到后端synchronize锁、Lock锁、借助Redis语法实现简单锁、Redis+Lua分布式锁、Redisson分布式锁,再到DB的悲观锁、乐观锁、借助表唯一索引等等都可以实现防重提交,以保证数据的安全性。

    这篇文章我们介绍其中一aJdNU种方案–借助Redis语法实现简单锁,最终实现防重提交。

    背景

    我们项目中,为了控制表单重复提交问题,会在点击页面按钮(向后端发起业务请求)后就会置灰按钮,直到后端响应后解除按钮置灰。通过按钮置灰来防止重启提交问题。但Postman、Jmeter和其他服务调用(绕过前端页面)呢?所以后端接口也要根据控制表单重复提交的问题。

    后端代码可以在2个位置做控制:

    一是放在gateway网关做:

    • 好处是只在一个地方加上控制代码,就可以控制所有接口的重复提交问题。
    • 坏处是控制的范围太广(比如查询接口无需控制,控制了反而多余)、定义重复提交的时间段不能灵活调整。

    二是放在AOP切面做:

    • 好处是只有需要的地方才会被控制(哪里需要引用一下自定义注解即可),另外也能灵活调整定义重复提交的时间段(自定义注解里定义时间字段开放给使用者填写)。
    • 坏处是每个需要控制的地方都要加注解,会有侵入性和一定的工作量。

    实现代码

    1、添加自定义注解

    package com.xxx.annotations;
    
    import Java.lang.annotation.*;
    
    /**
     * 自定义注解防止表单重复提交
     *
     * @Author WANGLINGQIANG
     * @Date 2023/9/6 10:11
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RepeatSubmit {
    
        /**
         * 过期时间,单位毫秒
         */
        long expireTime() default 500L;
    
    }

    2、添加AOP切面

    package com.xxx.aop;
    
    import com.xxx.annotations.RepeatSubmit;
    import com.xxx.exception.ServiceException;
    import lombok.extern.sl编程客栈f4j.Slf4j;
    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.aspectj.lang.reflect.MethodSignature;
    import org.springframework.data.redis.core.RedisTemplataJdNUe;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.annotation.Resource;
    import javax.servlet.http.HttpServletRequest;
    import java.lang.reflect.Method;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 防止表单重复提交切面
     *
     * @Author WANGLINGQIANG
     * @Date 2023/9/6 10:13
     */
    @Slf4j
    @Aspect
    @Component
    public class RepeatSubmitAspect {
        private static final String KEY_PREFIX = "repeat_submit:";
        @Resource
        private RedisTemplate redisTemplate;
    
        @Pointcut("@annotation(com.xxx.annotations.RepeatSubmit)")
        public void repeatSubmit() {}
    
        @Around("repeatSubmit()")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwablwww.devze.come {
     http://www.devze.com   	//joinPoint获取方法对象
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            //获取方法上的@RepeatSubmit注解
            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
            //获取HttpServletRequest对象,以获取请求uri
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = requestAttributes.getRequest();
            String uri = request.getRequestURI();
            //拼接Redis的key,这里只是简单根据uri来判断是否重复提交。可以根据自己业务调整,比如根据用户id或者请求token等
            String cacheKey = KEY_PREFIX.concat(uri);
            Boolean flag = null;
            try {
                //借助setIfAbsent(),key不存在才能设值成功
                flag = redisTemplate.opsForValue().setIfAbsent(cacheKey, "", annotation.expireTime(), TimeUnit.MILLISECONDS);
            } catch (Exception e) {
                //如果Redis不可用,则打印日志记录,但依然对请求放行
                log.error("", e);
                return joinPoint.proceed();
            }
            //Redis可用的情况,如果flag=true说明单位时间内这是第一次请求,放行
            if (flag) {
                return joinPoint.proceed();
            } else {
                //进入else说明单位时间内进行了多次请求,则拦截请求并提示稍后重试
                throw new ServiceException("系统繁忙,请稍后重试");
            }
        }
    }

    这里利用redisTemplate的setIfAbsent()实现的,如果存在就不能set成功,set的同时设置过期时间,可以是用使用默认,也可以自己根据业务调整。

    另外,cacheKey的定义,也可以根据自己的需要去调整,比如根据当前登录用户的userId、当前登录的token等。

    3、使用

    @Slf4j
    @RestController
    @RequestMapping("/user")
    public class UserController {
    
    	@RepeatSubmit
        @PostMapping
        public AJAXResult add(@Validated @RequestBody SysUser user) {
        	//....
        }
    

    场景二:控制接口调用频率

    背景

    忘记密码后通过发送手机验证码找回密码的场景。因为每发一条短信都需要收费,所以要控制发短信的频率。

    比如,同一个手机号在3分钟内只能发送3次短信,超过3次后则提示用户“短信发送过于频繁,请10分钟后再试”。

    实现代码

    @Slf4j
    @RestController
    @RequestMapping("/sms")
    public class SmsController {
        @Resource
        private ISmsService smsService;
        @Resource
        public RedisTemplate redisTemplate;
    
        @PostMapping("/sendValidCode")
        public Result sendValidCode(@RequestBody @Valid SmsDTO smsDTO) {
            //验证手机号格式
            checkPhoneNumber(smsDTO.getPhoneNumber());
            
            //...其他验证
            
    		//拼接Redis的key(key为手机号,以控制一个手机号有限时间内容发送的次数)
            String cacheKey = "sms:code:resetPwd:"+smsDTO.getPhoneNumber();
            //验证发送短信次数,超过则拦截(阈值是3次,超时时间是3分钟,重试时间是10分钟)
            checkSendCount(cacheKey, THRESHOLD, TIMEOUT, RETRY_TIME);
            return smsService.sendMsg(smsDTO);
        }
        
        /**
         * 验证发送短信次数,超过则拦截
         * 该方法用lua脚本替换实现更好
         */
        private void checkSendCount(String cacheKey, Long threshold, Long timeout, String retryTime) {
       		//首先进方法就先+1
            Long count = redisTemplate.opsForValue().increment(cacheKey);
            //然后比较次数,是否超过阈值
            if (count > threshold) {
                //超过则设置过期时间为10分钟,并提示10分钟后重试
                redisTemplate.expire(cacheKey, 10L, TimeUnit.MINUTES);
                throw new ServiceException("短信发送过于频繁,请" + retryTime + "分钟后再试");
            } else {
                //没超过3次,则累加上这一次
                redisTemplate.expire(cacheKey, timeout, TimeUnit.MINUTES);
            }
        }
    
    }

    总结

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新数据库

    数据库排行榜