在日常使用各种app或者登陆网站的时候,基本都会看到短信验证码这个功能,实现短信验证码的方式有很多,我这里给出基于SpringBoot+Redis的短信验证码实现方式
把一切都简化,短信验证码的实现无法就是下面几点:
1、后端随机生成四位到八位的随机数字作为短信的验证码,在redis中保存一定时间(一般是5分钟)。
2、调用第三方短信接口将短信验证码发给用户。
3、用户输入短信验证码提交后,在后端与之前生成的短信验证码作比较,如果相同说明验证成功,否则验证失败。
这样 一个很简单的短信验证手机号的接口就写好了 但是 这样简单的短信接口有着被恶意调用 也就是被刷的风险
常见的刷短信验证的行为有以下两种
1.以攻击手机号为目的刷短信验证码
这类攻击目标主要是攻击者借助web网站短信接口对目标手机号进行短信轰炸。攻击者会先收集互联网上多个未经防护的网站短信接口,设定要攻击的手机号码通过模拟用户,循环向后台发送短信验证码请求,达到攻击手机号的目的。
2.以恶意刷取目标网站短信费用为目的的攻击
这类攻击主要目的是刷掉目标网站的短信费用,在第一种基础上攻击者会不停变换各种接口参数如手机号、IP(采用高匿代理)等去请求后台发送短信验证码,进行恶意刷短信,后台根本无力辨别用户真伪。攻击目标明确,难以防护,因其变换不同IP、手机号,一些简单措施基本失效
针对这些攻击者 我们需要做出一些应对的措施
首先是增加图形验证码 当用户进行“获取短信验证码”操作前,弹出图形验证码 要求用户输入验证码后 服务器端才发送动态短信到用户手机上 但如果攻击者忽略验证码验证错误的情况 大量的执行请求会给服务器带来额外的负担 我建议在服务器端限制单个IP在单位时间内的请求次数 一旦用户请求次数超出设定的阈值 则停止对该IP一段时间的请求 若情节特别验严重的 也可以将该IP加入黑名单 最后就是限制重复发送动态短信的间隔时长 即当单个用户发送一次动态短信之后 服务器限制只有在一定时长之后 一般是60s 才能进行第二次动态短信请求
package com.huawei.roma.auth.verify.utils;
import redis.clients.jedis.Jedis;
import java.util.HashMap;
import java.util.Map;
/**
* redis hash 漏斗限流
*
* @author liaojiamin
* @Date:Created in 16:57 2020/5/29
*/
public class FunnelRateLimiter {
private Jedis client;
public FunnelRateLimiter(Jedis client) {
this.client = client;
}
/**
* 请求是否成功
*/
public boolean isActionAllowed(String userId, String actionKey, int capacity, float leakingRate, int quota) {
String key = this.key(userId, actionKey);
long nowTs = System.currentTimeMillis();
Map<String, String> funnelMap = client.hgetAll(key);
if (funnelMap == null || funnelMap.isEmpty()) {
return initFunnel(key, nowTs, capacity, quota);
}
long intervalTs = nowTs - Long.parseLong(funnelMap.get("leakingTs"));
int intervalCapacity = (int) (intervalTs * leakingRate);
// 时间过长, int可能溢出
if (intervalCapacity < 0) {
intervalCapacity = 0;
initFunnel(key, nowTs, capacity, quota);
}
// 腾出空间必须 >= 1
if (intervalCapacity < 1) {
intervalCapacity = 0;
}
int leftCapacity = Integer.parseInt(funnelMap.get("leftCapacity")) + intervalCapacity;
if (leftCapacity > capacity) {
leftCapacity = capacity;
}
return initFunnel(key, nowTs, leftCapacity, quota);
}
/**
* 存入redis,初始funnel
*/
private boolean initFunnel(String key, long nowTs, int capacity, int quota) {
Map<String, String> funnelMap = new HashMap<>();
funnelMap.put("leftCapacity", String.valueOf((capacity > quota) ? (capacity - quota) : 0));
funnelMap.put("leakingTs", String.valueOf(nowTs));
client.hmset(key, funnelMap);
return capacity >= quota;
}
/**
* 限流key
*/
private String key(String userId, String actionKey) {
return String.format("limit:%s:%s", userId, actionKey);
}
public static void main(String[] args) throws InterruptedException {
Jedis jedis = new Jedis("127.0.0.1", 6379);
FunnelRateLimiter limiter = new FunnelRateLimiter(jedis);
for (int i = 1; i <= 30; i++) {
boolean success = limiter.isActionAllowed("liziba", "view", 40, 20f / 86400000f, 1);
System.out.println("第" + i + "请求" + (success ? "成功" : "失败"));
}
}
}