• spring boot:用redis+lua限制短信验证码的发送频率(spring boot 2.3.2)


    一,为什么要限制短信验证码的发送频率?

    1,短信验证码每条短信都有成本制约,

       肯定不能被刷接口的乱发

       而且接口被刷会影响到用户的体验,

       影响服务端的正常访问,

       所以既使有图形验证码等的保护,

       我们仍然要限制短信验证码的发送频率

    2,演示项目中我使用的数值是:

       同一手机号60秒内禁止重复发送

       同一手机号一天时间最多发10条

       验证码的有效时间是300秒

       大家可以根据自己的业务需求进行调整 

    3,生产环境中使用时对表单还需要添加参数的验证/反csrf/表单的幂等检验等,

       本文仅供参考

    说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://www.cnblogs.com/architectforest

             对应的源码可以访问这里获取: https://github.com/liuhongdi/

    说明:作者:刘宏缔 邮箱: 371125307@qq.com

    二,演示项目的相关信息

     1,项目地址:

    https://github.com/liuhongdi/sendsms

    2,项目功能说明:

      用redis保存验证码的数据和实现时间控制

      发送短信功能我使用的是luosimao的sdk,

      大家可以根据自己的实际情况修改

    3,项目结构,如图:

    三,配置文件说明

    1,pom.xml

            <!--redis begin-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-pool2</artifactId>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-core</artifactId>
                <version>2.11.1</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>2.11.1</version>
            </dependency>
            <!--redis   end-->
    
            <!--luosimao send sms begin-->
            <dependency>
                <groupId>com.sun.jersey</groupId>
                <artifactId>api</artifactId>
                <version>1.19</version>
                <scope>system</scope>
                <systemPath>${project.basedir}/src/main/resources/jar/jersey-bundle-1.19.jar</systemPath>
            </dependency>
            <dependency>
                <groupId>org.json</groupId>
                <artifactId>json</artifactId>
                <version>1.0</version>
                <scope>system</scope>
                <systemPath>${project.basedir}/src/main/resources/jar/json-org.jar</systemPath>
            </dependency>
            <!--luosimao send sms   end-->

    说明:引入了发短信的sdk和redis访问依赖

    2,application.properties

    #error
    server.error.include-stacktrace=always
    #errorlog
    logging.level.org.springframework.web=trace
    
    #redis
    spring.redis.host=127.0.0.1
    spring.redis.port=6379
    spring.redis.password=lhddemo
    
    #redis-lettuce
    spring.redis.lettuce.pool.max-active=8
    spring.redis.lettuce.pool.max-wait=1
    spring.redis.lettuce.pool.max-idle=8
    spring.redis.lettuce.pool.min-idle=0

    配置了redis的访问

    四,lua代码说明

    1,smslimit.lua

    local key = KEYS[1]
    local keyseconds = tonumber(KEYS[2])
    local daycount = tonumber(KEYS[3])
    local keymobile = 'SmsAuthKey:'..key
    local keycount = 'SmsAuthCount:'..key
    --redis.log(redis.LOG_NOTICE,' keyseconds: '..keyseconds..';daycount:'..daycount)
    local current = redis.call('GET', keymobile)
    --redis.log(redis.LOG_NOTICE,' current: keymobile:'..current)
    if current == false then
       --redis.log(redis.LOG_NOTICE,keymobile..' is nil ')
       local count = redis.call('GET', keycount)
       if count == false then
          redis.call('SET', keycount,1)
          redis.call('EXPIRE',keycount,86400)
    
          redis.call('SET', keymobile,1)
          redis.call('EXPIRE',keymobile,keyseconds)
          return '1'
       else
          local num_count = tonumber(count)
          if num_count+1 > daycount then
             return '2'
          else
             redis.call('INCRBY',keycount,1)
    
             redis.call('SET', keymobile,1)
             redis.call('EXPIRE',keymobile,keyseconds)
             return '1'
          end
       end
    else
       --redis.log(redis.LOG_NOTICE,keymobile..' is not nil ')
       return '0'
    end

    说明:每天不超过指定的验证码短信条数,并且60秒内没有发过知信,

             才返回1,表示可以发

            返回2:表示条数已超

            返回0:表示上一条短信发完还没超过60秒

    五,java代码说明

    1,RedisLuaUtil.java

    @Service
    public class RedisLuaUtil {
        @Resource
        private StringRedisTemplate stringRedisTemplate;
        //private static final Logger logger = LogManager.getLogger("bussniesslog");
        /*
        run a lua script
        luaFileName: lua file name,no path
        keyList: list for redis key
        return 0: fail
               1: success
        */
        public String runLuaScript(String luaFileName, List<String> keyList) {
            DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/"+luaFileName)));
            redisScript.setResultType(String.class);
            String result = "";
            String argsone = "none";
            try {
                result = stringRedisTemplate.execute(redisScript, keyList,argsone);
            } catch (Exception e) {
                //logger.error("发生异常",e);
            }
            return result;
        }
    }

    用来调用lua程序

    2,AuthCodeUtil.java

    @Component
    public class AuthCodeUtil {
    
        //验证码长度
        private static final int AUTHCODE_LENGTH = 6;
        //验证码的有效时间300秒
        private static final int AUTHCODE_TTL_SECONDS = 300;
        private static final String AUTHCODE_PREFIX = "AuthCode:";
    
        @Resource
        private RedisTemplate redisTemplate;
    
        //get a auth code
        public String getAuthCodeCache(String mobile){
            String authcode = (String) redisTemplate.opsForValue().get(AUTHCODE_PREFIX+mobile);
            return authcode;
        }
    
        //把验证码保存到缓存
        public void setAuthCodeCache(String mobile,String authcode){
            redisTemplate.opsForValue().set(AUTHCODE_PREFIX+mobile,authcode,AUTHCODE_TTL_SECONDS, TimeUnit.SECONDS);
        }
    
        //make a auth code
        public static String newAuthCode(){
            String code = "";
            Random random = new Random();
            for (int i = 0; i < AUTHCODE_LENGTH; i++) {
                //设置了bound参数后,取值范围为[0, bound),如果不写参数,则取值为int范围,-2^31 ~ 2^31-1
                code += random.nextInt(10);
            }
            return code;
        }
    }

    生成验证码、保存验证码到redis、从redis获取验证码

    3,SmsUtil.java

    @Component
    public class SmsUtil {
        @Resource
        private RedisLuaUtil redisLuaUtil;
       //发送验证码的规则:同一手机号:
        //60秒内不允许重复发送
         private static final String SEND_SECONDS = "60";
        //一天内最多发10条
         private static final String DAY_COUNT = "10";
        //密钥
        private static final String SMS_APP_SECRET = "key-thisisademonotarealappsecret";
    
         //发送验证码短信
        public String sendAuthCodeSms(String mobile,String authcode){
    
            Client client = Client.create();
            client.addFilter(new HTTPBasicAuthFilter(
                    "api",SMS_APP_SECRET));
            WebResource webResource = client.resource(
                    "http://sms-api.luosimao.com/v1/send.json");
            MultivaluedMapImpl formData = new MultivaluedMapImpl();
            formData.add("mobile", mobile);
            formData.add("message", "验证码:"+authcode+"【商城】");
            ClientResponse response =  webResource.type( MediaType.APPLICATION_FORM_URLENCODED ).
                    post(ClientResponse.class, formData);
            String textEntity = response.getEntity(String.class);
            int status = response.getStatus();
            return "短信已发送";
        }
    
        //判断一个手机号能否发验证码短信
        public String isAuthCodeCanSend(String mobile) {
            List<String> keyList = new ArrayList();
            keyList.add(mobile);
            keyList.add(SEND_SECONDS);
            keyList.add(DAY_COUNT);
            String res = redisLuaUtil.runLuaScript("smslimit.lua",keyList);
            System.out.println("------------------lua res:"+res);
            return res;
        }
    }

    判断短信是否可以发送、发送短信

    4,RedisConfig.java

    @Configuration
    public class RedisConfig {
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
            //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
            redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
            redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
            //使用StringRedisSerializer来序列化和反序列化redis的ke
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
            //开启事务
            redisTemplate.setEnableTransactionSupport(true);
            redisTemplate.setConnectionFactory(redisConnectionFactory);
            return redisTemplate;
        }
    }

    配置redis的访问

    5,HomeController.java

    @RestController
    @RequestMapping("/home")
    public class HomeController {
        @Resource
        private SmsUtil smsUtil;
        @Resource
        private AuthCodeUtil authCodeUtil;
    
        //发送一条验证码短信
        @GetMapping("/send")
        public String send(@RequestParam(value="mobile",required = true,defaultValue = "") String mobile) {
    
             String returnStr = "";
             String res = smsUtil.isAuthCodeCanSend(mobile);
             if (res.equals("1")) {
                 //生成一个验证码
                 String authcode=authCodeUtil.newAuthCode();
                 //把验证码保存到缓存
                 authCodeUtil.setAuthCodeCache(mobile,authcode);
                 //发送短信
                 return smsUtil.sendAuthCodeSms(mobile,authcode);
             } else if (res.equals("0")) {
                 returnStr = "请超过60秒之后再发短信";
             } else if (res.equals("2")) {
                 returnStr = "当前手机号本日内发送数量已超限制";
             }
             return returnStr;
        }
    
        //检查验证码是否正确
        @GetMapping("/auth")
        public String auth(@RequestParam(value="mobile",required = true,defaultValue = "") String mobile,
                           @RequestParam(value="authcode",required = true,defaultValue = "") String authcode) {
            String returnStr = "";
            String authCodeCache = authCodeUtil.getAuthCodeCache(mobile);
            System.out.println(":"+authCodeCache+":");
            if (authCodeCache.equals(authcode)) {
                returnStr = "验证码正确";
            } else {
                returnStr = "验证码错误";
            }
            return returnStr;
        }
    }

    发验证码和检测验证码是否有效

    六,效果测试

    1,访问:(注意换成自己的手机号)

    http://127.0.0.1:8080/home/send?mobile=13888888888

    返回:

    短信已发送

    60秒内连续刷新返回:

    请超过60秒之后再发短信

    如果超过10条时返回:

    当前手机号本日内发送数量已超限制

    2,验证:

    http://127.0.0.1:8080/home/auth?mobile=13888888888&authcode=638651

    如果有效会返回:

    验证码正确

    七,查看spring boot的版本:

      .   ____          _            __ _ _
     /\ / ___'_ __ _ _(_)_ __  __ _    
    ( ( )\___ | '_ | '_| | '_ / _` |    
     \/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
     =========|_|==============|___/=/_/_/_/
     :: Spring Boot ::        (v2.3.2.RELEASE)
  • 相关阅读:
    php错误抑制符
    php执行运算符
    php中一个经典的!==的用法
    php实现简单验证码的功能
    jquery是什么
    php连接符
    php与java语法的区别
    考雅思策略
    php魔术常量
    PHP中数据类型转换的三种方式
  • 原文地址:https://www.cnblogs.com/architectforest/p/13432987.html
Copyright © 2020-2023  润新知