spring boot: use redis+lua to limit the frequency of sending SMS verification codes (spring boot 2.3.2)

1. Why limit the sending frequency of SMS verification codes?

1, SMS verification code Each SMS has cost constraints,

Certainly can not be brushed the random hair of the interface

And the interface is brushed will affect the user's experience,

Affect the normal access of the server,

So even with the protection of graphic verification codes, etc.,

We still have to limit how often SMS verification codes are sent

 

2. The values ​​I use in the demo project are:

Repeated sending of the same phone number within 60 seconds is prohibited

The same mobile number can send up to 10 messages a day

The valid time of the verification code is 300 seconds

You can adjust it according to your business needs

 

3. When using in the production environment, it is necessary to add parameter validation/anti-csrf/form idempotency check to the form, etc.

This article is for reference only

 

Description: Liu Hongdi's Architecture Forest is a blog focusing on architecture. The address is: https://www.cnblogs.com/architectforest

The corresponding source code can be obtained here: https://github.com/liuhongdi/

Description: Author: Liu Hongdi Email: 371125307@qq.com

 

2. Information about the demo project

1, Project address:

https://github.com/liuhongdi/sendsms

 

2, project function description:

Use redis to save verification code data and implement time control

I use luosimao's sdk for sending SMS,

You can modify it according to your actual situation

 

3. Project structure, as shown in the figure:

Three, configuration file description

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-->

Description: Introduced sdk and redis access dependencies for texting

 

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

Configured access to redis

 

Four, lua code description

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

Note: The number of SMS verification codes should not exceed the specified number per day, and no notification letter has been sent within 60 seconds.

only returns 1, indicating that it can be sent

Return 2: Indicates that the number of bars has exceeded

Return 0: It means that the last text message has not been sent more than 60 seconds

 

Five, java code description

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("An exception occurs",e);
        }
        return result;
    }
}

Used to call lua programs

 

2,AuthCodeUtil.java

@Component
public class AuthCodeUtil {

    //verification code length
    private static final int AUTHCODE_LENGTH = 6;
    //The valid time of the verification code is 300 seconds
    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;
    }

    //Save the verification code to the cache
    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++) {
            //already setup bound After the parameter, the value range is[0, bound),If no parameter is written, the value is int scope,-2^31 ~ 2^31-1
            code += random.nextInt(10);
        }
        return code;
    }
}

Generate verification code, save verification code to redis, get verification code from redis

 

3,SmsUtil.java

@Component
public class SmsUtil {
    @Resource
    private RedisLuaUtil redisLuaUtil;
   //Rules for sending verification codes: same mobile phone number:
    //60 Duplicate sending is not allowed within seconds
     private static final String SEND_SECONDS = "60";
    //Up to 10 posts in one day
     private static final String DAY_COUNT = "10";
    //key
    private static final String SMS_APP_SECRET = "key-thisisademonotarealappsecret";

     //Send verification code SMS
    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", "Verification code:"+authcode+"[Mall]");
        ClientResponse response =  webResource.type( MediaType.APPLICATION_FORM_URLENCODED ).
                post(ClientResponse.class, formData);
        String textEntity = response.getEntity(String.class);
        int status = response.getStatus();
        return "SMS sent";
    }

    //Determine whether a mobile phone number can send a verification code SMS
    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;
    }
}

Determine whether SMS can be sent, send SMS

 

4,RedisConfig.java

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        //use Jackson2JsonRedisSerializer to serialize and deserialize redis of value value
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        //use StringRedisSerializer to serialize and deserialize redis of ke
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //open transaction
        redisTemplate.setEnableTransactionSupport(true);
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

Configure access to redis

 

5,HomeController.java

@RestController
@RequestMapping("/home")
public class HomeController {
    @Resource
    private SmsUtil smsUtil;
    @Resource
    private AuthCodeUtil authCodeUtil;

    //Send a verification code SMS
    @GetMapping("/send")
    public String send(@RequestParam(value="mobile",required = true,defaultValue = "") String mobile) {

         String returnStr = "";
         String res = smsUtil.isAuthCodeCanSend(mobile);
         if (res.equals("1")) {
             //generate a verification code
             String authcode=authCodeUtil.newAuthCode();
             //Save the verification code to the cache
             authCodeUtil.setAuthCodeCache(mobile,authcode);
             //send messages
             return smsUtil.sendAuthCodeSms(mobile,authcode);
         } else if (res.equals("0")) {
             returnStr = "Please wait more than 60 seconds before texting";
         } else if (res.equals("2")) {
             returnStr = "The current mobile phone number has exceeded the limit of sending in this day";
         }
         return returnStr;
    }

    //Check if the verification code is correct
    @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 = "The verification code is correct";
        } else {
            returnStr = "Verification code error";
        }
        return returnStr;
    }
}

Send verification code and check whether the verification code is valid

 

Six, the effect test

1, visit: (note to replace your mobile phone number)

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

return:

SMS sent

Continuous refresh within 60 seconds returns:

Please wait more than 60 seconds before texting

If there are more than 10, return:

The current mobile phone number has exceeded the limit of sending in this day

 

2, Verify:

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

If valid it will return:

The verification code is correct

 

Seven, check the version of spring boot:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.2.RELEASE)

 

Tags: Java Redis Spring Spring Boot lua sms

Posted by shashiku on Wed, 25 May 2022 21:42:39 +0300