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)
spring boot:用redis+lua限制短信验证码的发送频率(spring boot 2.3.2)的更多相关文章
- php 阿里云短信服务及阿里大鱼实现短信验证码的发送
一:使用阿里云的短信服务 ① 申请短信签名 ②申请短信模板 ③创建Access Key,获取AccessKeyId 与 AccessKeySecret.(为了安全起见,这里建议使用子用户的Access ...
- 用Laravel Sms实现 laravel短信验证码的发送
使用Laravel Sms这个扩展包实现短信验证码的发送,这里以阿里云的短信服务为例: 首先,要创建短信签名和短信模板,具体申请详情如下, 接下来,需要创建AccessKey,由于AccessKey是 ...
- soapui调用redis,获取短信验证码
1.首先,调用redis需要引入redis的jar包,放入到soapui指定目录中,例如我的目录D:\Program Files\SmartBear\SoapUI-Pro-5.1.2\bin\ext ...
- Django商城项目笔记No.5用户部分-注册接口-短信验证码
Django商城项目笔记No.4用户部分-注册接口-短信验证码 短信验证码也保存在redis里(sms_code_15101234567) 在views中新增SMSCodeView类视图,并且写出步骤 ...
- 使用聚合数据API查询快递数据-短信验证码-企业核名
有位朋友让我给他新开的网站帮忙做几个小功能,如下: 输入快递公司.快递单号,查询出这个快件的所有动态(从哪里出发,到了哪里) 在注册.登录等场景下的手机验证码(要求有一定的防刷策略) 通过输入公司名的 ...
- thinkphp结合云之讯做短信验证码
thinkphp结合云之讯做短信验证码先去云之讯注册账号 网址http://www.ucpaas.com/ 注册云之讯平台账号,即可免费获得10元测试费用测试够用啦 解压附件到 ThinkPHP\Li ...
- Spring Security构建Rest服务-1203-Spring Security OAuth开发APP认证框架之短信验证码登录
浏览器模式下验证码存储策略 浏览器模式下,生成的短信验证码或者图形验证码是存在session里的,用户接收到验证码后携带过来做校验. APP模式下验证码存储策略 在app场景下里是没有cookie信息 ...
- springboot +spring security4 自定义手机号码+短信验证码登录
spring security 默认登录方式都是用户名+密码登录,项目中使用手机+ 短信验证码登录, 没办法,只能实现修改: 需要修改的地方: 1 .自定义 AuthenticationProvide ...
- SpringBoot + Spring Security 学习笔记(五)实现短信验证码+登录功能
在 Spring Security 中基于表单的认证模式,默认就是密码帐号登录认证,那么对于短信验证码+登录的方式,Spring Security 没有现成的接口可以使用,所以需要自己的封装一个类似的 ...
随机推荐
- 【NOIP2014模拟】高级打字机
题目描述 早苗入手了最新的高级打字机.最新款自然有着与以往不同的功能,那就是它具备撤销功能,厉害吧. 请为这种高级打字机设计一个程序,支持如下3种操作: T x:在文章末尾打下一个小写字母x.(typ ...
- python中的算数运算符+、-、*、/、//、%、**
例如a=5,b=2 + 两个对象相加 a+b=7 - 两个对象相减 a- ...
- Netty之旅三:Netty服务端启动源码分析,一梭子带走!
Netty服务端启动流程源码分析 前记 哈喽,自从上篇<Netty之旅二:口口相传的高性能Netty到底是什么?>后,迟迟两周才开启今天的Netty源码系列.源码分析的第一篇文章,下一篇我 ...
- [LeetCode]301. 删除无效的括号(DFS)
题目 题解 step1. 遍历一遍,维护left.right计数器,分别记录不合法的左括号.右括号数量. 判断不合法的方法? left维护未匹配左括号数量(增,减)(当left为0遇到右括号,则交由r ...
- 【Flutter 实战】文件系统目录
老孟导读:Flutter 中获取文件路径,我们都知道使用 path_provider,但对其目录对含义不是很清楚,此文介绍 Android.iOS 系统的文件目录,不同场景下建议使用的目录. 不同的平 ...
- Java架构师方案—多数据源开发详解及原理(二)(附完整项目代码)
1. mybatis下数据源开发工作 2. 数据源与DAO的关系原理模型 3. 为什么要配置SqlSessionTemplate类的bean 4. 多数据源应用测试 1. mybatis下数据源开发工 ...
- Docker实战(4):Docker错误记一笔
创建容器的时候报错WARNING: IPv4 forwarding is disabled. Networking will not work. 解决办法: vim /usr/lib/sysctl.d ...
- 【原创】一层Nginx反向代理K8S化部署实践
目录: 1)背景介绍 2)方案分析 3)实现细节 4)监控告警 5)日志收集 6)测试 一.背景介绍 如下图所示,传统方式部署一层Nginx,随着业务扩大,维护管理变得复杂,繁琐,耗时耗力和易 ...
- 005.操作系统及Linux系统,虚拟机的作用和发展历史
操作系统及其作用 操作系统发展史 Linux系统 虚拟机 操作系统 操作系统 操作系统的作用 不同领域的主流操作系统 操作系统(Operation System,OS) 操作系统作为接口的示意图 没有 ...
- 聊聊分布式下的WebSocket解决方案
前言 最近王子自己搭建了个项目,项目本身很简单,但是里面有使用WebSocket进行消息提醒的功能,大体情况是这样的. 发布消息者在系统中发送消息,实时的把消息推送给对应的一个部门下的所有人. 这里面 ...