一,為什么要限制短信驗證碼的發送頻率?
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)
