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

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)

spring boot:用redis+lua限制短信验证码的发送频率(spring boot 2.3.2)的更多相关文章

  1. php 阿里云短信服务及阿里大鱼实现短信验证码的发送

    一:使用阿里云的短信服务 ① 申请短信签名 ②申请短信模板 ③创建Access Key,获取AccessKeyId 与 AccessKeySecret.(为了安全起见,这里建议使用子用户的Access ...

  2. 用Laravel Sms实现 laravel短信验证码的发送

    使用Laravel Sms这个扩展包实现短信验证码的发送,这里以阿里云的短信服务为例: 首先,要创建短信签名和短信模板,具体申请详情如下, 接下来,需要创建AccessKey,由于AccessKey是 ...

  3. soapui调用redis,获取短信验证码

    1.首先,调用redis需要引入redis的jar包,放入到soapui指定目录中,例如我的目录D:\Program Files\SmartBear\SoapUI-Pro-5.1.2\bin\ext ...

  4. Django商城项目笔记No.5用户部分-注册接口-短信验证码

    Django商城项目笔记No.4用户部分-注册接口-短信验证码 短信验证码也保存在redis里(sms_code_15101234567) 在views中新增SMSCodeView类视图,并且写出步骤 ...

  5. 使用聚合数据API查询快递数据-短信验证码-企业核名

    有位朋友让我给他新开的网站帮忙做几个小功能,如下: 输入快递公司.快递单号,查询出这个快件的所有动态(从哪里出发,到了哪里) 在注册.登录等场景下的手机验证码(要求有一定的防刷策略) 通过输入公司名的 ...

  6. thinkphp结合云之讯做短信验证码

    thinkphp结合云之讯做短信验证码先去云之讯注册账号 网址http://www.ucpaas.com/ 注册云之讯平台账号,即可免费获得10元测试费用测试够用啦 解压附件到 ThinkPHP\Li ...

  7. Spring Security构建Rest服务-1203-Spring Security OAuth开发APP认证框架之短信验证码登录

    浏览器模式下验证码存储策略 浏览器模式下,生成的短信验证码或者图形验证码是存在session里的,用户接收到验证码后携带过来做校验. APP模式下验证码存储策略 在app场景下里是没有cookie信息 ...

  8. springboot +spring security4 自定义手机号码+短信验证码登录

    spring security 默认登录方式都是用户名+密码登录,项目中使用手机+ 短信验证码登录, 没办法,只能实现修改: 需要修改的地方: 1 .自定义 AuthenticationProvide ...

  9. SpringBoot + Spring Security 学习笔记(五)实现短信验证码+登录功能

    在 Spring Security 中基于表单的认证模式,默认就是密码帐号登录认证,那么对于短信验证码+登录的方式,Spring Security 没有现成的接口可以使用,所以需要自己的封装一个类似的 ...

随机推荐

  1. delphi DBgrid应用全书

    在一个Dbgrid中显示多数据库    在数据库编程中,不必要也不可能将应用程序操作的所有数据库字段放入一个数据库文件中.正确的数据库结构应是:将数据库字段放入多个数据库文件,相关的数据库都包含一个唯 ...

  2. Java链接db2相关

    端口一般是50000或者60000 后面跟的可能是库名(个人猜测) 还有db2jcc.jar安装db2的可以从相应目录下载未安装的可以…(啥时候有空再传上来)

  3. 第18课 - make 中的路径搜索(下)

    第18课 - make 中的路径搜索(下) 1. 问题一 当 VPATH 和 vpath 同时出现,make 会如何处理? 工程项目的目录结构如下图所示,src1 和 src2 中都包含了 func. ...

  4. Centos6.5 离线 Openssh 升级

    目录 OpenSSH 升级 一.基于 Dropbear 设置备用 ssh 服务器 二. Openssh 更新 2.1 ssh配置 备份 2.2 openssh 升级 Openssl 升级(由于Open ...

  5. Catalina 动态壁纸相关设置

    关闭SIP 重启,在开机时一直按Command+r进入recovery模式. 打开终端,如图所示: 在终端中输入命令,回车: csrutil disable 然后重启 设置动态壁纸 首先需在Dynam ...

  6. python程序控制--分支结构

    单分支结构 单分支结构猜数字 二分支结构 多分支结构   注意多条件之间的包含关系 注意变量取值范围的覆盖 条件判断及组合 程序的异常处理 输入一个整数,进行乘方操作, 但是,如果用户没有输入整数的时 ...

  7. 智慧出行--maas

    未来智慧出行新生态——MaaS系统的解读与畅想 -城市交通:观察与思考 - 未来智慧出行新生态——MaaS系统的解读与畅想 在货运领域,有一种承运方式叫“多式联运”,它是由承运人与货主签订一份货运合同 ...

  8. CentOS7使用yum时File contains no section headers.解决办法

    本文转载于  https://blog.csdn.net/trokey/article/details/84908838 安装好CenOS7后,自带的yum不能直接使用,使用会出现如下问题: 原因是没 ...

  9. 30种SQL语句优化

    1.'对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引. 2.应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用 ...

  10. Jboss未授权访问漏洞复现

    一.前言 漏洞原因:在低版本中,默认可以访问Jboss web控制台(http://127.0.0.1:8080/jmx-console),无需用户名和密码. 二.环境配置 使用docker搭建环境 ...