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)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM