项目原来使用的短信验证码接口没有做安全限制,去网上查找了很多的解决方案,无非是在接口调用时添加图形验证码、单ip请求限制、限定每天每个号码获取短信验证码的次数、限制短信验证码的调用频率等。逛了一大圈,发现大家都只是信心满满的分享着一大堆逻辑和方法,至于具体的代码实现,就。。。大概大神们都觉得这东西没有什么技术含量吧。但是我觉得,所有的技术无论高低,业务不管复杂简单,都应该得到尊重,都有被分享的意义。因为在未来漫长的岁月中,总有人会因为这次的分享而得到帮助,哪怕只有一点点。刚好借着这次机会,分享本人一时性起写出的demo(只有逻辑哦,核心业务用你自己的就好),希望此贴之后,越来越多更牛逼的和更完善的demo能被各路高手实现和分享出来,也不枉我辈青年才俊将最美好的十年给了 IT 这一行!
下面请看业务层的代码:
package demo;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.alibaba.fastjson.JSON;
/**
* @author hqq
*
*/
@Service("mobileService")
public class MobileServiceImpl implements MobileService {
private static final Logger logger = Logger.getLogger(MobileServiceImpl.class);
@Override
public Result findCode(String mobile, String type, String imgCode, HttpServletRequest request) {
//mobile字段是传入的用户名,即手机号,必传字段;
//type是传入的需要发送的短信类型,必传字段,如登录,注册等(也可以不区分,按个人爱好喽);
//imgCode是传入的图形验证码,非必传字段,因为只有今日第四次调用该接口时才需要校验图形验证码
Result result = new Result();
//校验请求参数是否正确
if (StringUtils.isBlank(mobile) || StringUtils.isBlank(type)) {
result.setSuccess(false);
result.setMsg("请求参数不全");
return result;
}
mobile = mobile.trim();
// 判断传入的手机号格式是否正确
if (mobile.length() != 11 || !MobileUtil.isMobileNum(mobile)) {
result.setSuccess(false);
result.setMsg("手机号格式不正确");
return result;
}
// 要发送的短信验证码,生成六位数字验证码
String mobileCode = (int) ((Math.random() * 9 + 1) * 100000) + "";
String modelCode = null;//我这里使用的是阿里云的短信服务,
//调用阿里云的短信接口需要传入一个模板code参数,
//这个code再你申请短信模板时就会产生,且固定的值
//判断当前要发送的是哪种类型的短信,不同的类型的验证码应该进行区分,这样可以提高
//用户体验;区分的参数由调用者(前端开发人员)传入,通常不会出现参数不存在的问题
switch (type) {
case "register":// 发送注册的短信验证码
//核心业务隐身符1,用手机号去自己数据库查询当前用户是否存在,
//如果存在,则不能发送该类手机验证码,提示用户直接登录
if(手机号已注册){//伪代码
result.setSuccess(false);
result.setMsg("当前手机号已注册,请直接登录");
return result;
}
modelCode = "SMS_123456788";
break;
case "reset":// 发送重置登录密码的短信验证码
//核心业务隐身符1,用手机号去自己数据库查询当前用户是否存在,
//如果不存在,则不能发送该类手机验证码,提示用户注册
if(手机号未注册){//伪代码
result.setSuccess(false);
result.setMsg("当前手机号未注册,请先注册");
return result;
}
modelCode = "SMS_987654321";
break;
default:
result.setSuccess(false);
result.setMsg("非法请求");
return result;
}
String mobileKey = type+"_mobile_" + mobile;
String todayKey = "today_mobile_code_times_" + mobile;
// 验证码三十分钟内有效,并且距离上一次发送要超过2分钟的时间才能重新发送
Long times = RedisUtils.ttl(mobileKey);
if (times > 60 * 28) {
result.setSuccess(false);
result.setMsg("距离您上次发送验证码不足两分钟,请两分钟后再尝试获取");
return result;
}
// 判断当前手机号今天发送密码次数是否已达上线,每天15条(具体条数根据自己的需求调用)
String todayTimes = RedisUtils.get(todayKey);
int todayCount = 1;
if (todayTimes != null) {
todayCount = new Integer(todayTimes);
if (todayCount >= 15) {
//此时还可以记录当前用户的手机号,ip,调用的短信验证码类型到表中,
//方便系统记录与分析。系统可以分析该用户该周该月调用短信接口的次数
//由此来分析该ip的用户是否是正常的用户,如果调用太频繁,
//比如连续一周或数周都在调用该接口,系统可以暂时禁用该ip发来的请求,
//或者降低该手机号获取短信验证码的次数一般大网站通常都得使用大数据来监控了,
//而小网站,就没必要整的这么复杂了
result.setSuccess(false);
result.setMsg("当前手机号今日发送验证码已达上限,请明日再来");
return result;
}
todayCount++;
}
//今天发送短信超过三次,再次调用接口时,需要调谷歌图形验证码
if(todayCount>3){
result.setStatus(1);//只要今日获取验证码次数超过三次,
//之后每次获取都要谷歌验证码,这个标识返回给前端,
//前端看到这个值,需要调用谷歌图形验证码,
//待用户输入图形验证码后才能调用该接口
if(StringUtils.isBlank(imgCode)){
result.setSuccess(false);
result.setMsg("为保证您账号安全,本次请求需要输入图形验证码");
return result;
}
// 检验图形验证码
String kapchatKey =type+ "_kaptcha_" + mobile;
String kapchat = RedisUtils.get(kapchatKey);//获取redis数据库保存的谷歌图形验证码
if (kapchat == null) {
result.setMsg("图形验证码已失效,请重新输入");
result.setSuccess(false);
return result;
} else if (!kapchat.equals(imgCode.toLowerCase())) {
result.setSuccess(false);
result.setMsg("您输入的验证码错误,请重新输入");
return result;
}
}else if(todayCount==3){
result.setStatus(1);//这已是第三次调用,下次调用时,就得传入谷歌验证码
}
String msg = "";//发送短信验证码是否成功与失败
try {
//发送短信验证码,请求成功后返回指定标识,请求失败,可以返回失败的信息,
//方便开发人员排查bug。此处使用的是阿里云的短信服务,
//你也可以使用其他的短信服务,此处不做赘述
msg = MobileCodeUtils.sendCode(mobile, modelCode, mobileCode);
logger.info("手机号:" + mobile + " 的验证码是:" + mobileCode);
if (msg != null && "SUCCESS".equals(msg)) {
result.setSuccess(true);
result.setMsg("您的手机验证码发送成功,请注意查收,本验证码30分钟内有效");
// 保存验证码到redis
RedisUtils.set(mobileKey, mobileCode, 60 * 30 + 5);//redis中的code比实际要多5秒
// 记录本号码发送验证码次数
RedisUtils.set(todayKey, todayCount + "", MobileUtil.getSurplusTime());
// 删除图形验证码
RedisUtils.del(kapchatKey);
} else {
result.setSuccess(false);
result.setMsg("短信验证码发送失败:" + msg);
return result;
}
} catch (Exception e) {
result.setSuccess(false);
result.setMsg("获取短信验证码异常:" + e.getMessage());
logger.info("获取手机验证码异常:" + e.getMessage());
return result;
}
//此处需要添加操作流水,记录哪个手机号,哪个ip,哪个时间调用了哪种类型的接口
return result;
}
}
获取谷歌kaptcha图形验证码的方式请看我的上一篇博客:https://blog.csdn.net/weixin_42023666/article/details/89561592
RedisUtils.java工具类请看我的另一篇博客:https://blog.csdn.net/weixin_42023666/article/details/89287418
MobileCodeUtils.java是封装好的调用第三方短信验证码的工具类,此时我使用的是阿里云的短信服务,各位只需将自己项目原来的短信工具类进行修改,只要能保证发送短信的功能就可以了。具体的不赘述,毕竟不是本帖的重点,阿里云短信服务的接入可以参考这篇博文:https://blog.csdn.net/weixin_42023666/article/details/101770229 ,也可自行百度。
然后是MobileUtil.java类:
package mobile;
import java.util.regex.Pattern;
import java.util.Calendar;
import java.util.Date;
import org.apache.commons.lang.StringUtils;
/**
* @ClassName: MobileUtil
* @author hqq
*/
public class MobileUtil {
/**
* 正则表达式:验证手机号
*/
private static final String REGEX_MOBILE = "^((13[0-9])|(15[^4,\\D])|(18[0-3,5-9])|(17[0-9]))\\d{8}$";
/**
* 判断是否是手机号格式,如果传入的是空串,返回false
* @param mobile
* @return 校验通过返回true,否则返回false
*/
public static boolean isMobileNum(String mobile) {
if(StringUtils.isBlank(mobile)){
return false;
}
return Pattern.matches(REGEX_MOBILE, mobile);
}
/**
* 获取今日的剩余时间,返回值单位:秒
* @return
*/
public static Integer getSurplusTime(){
Calendar c = Calendar.getInstance();
long now = c.getTimeInMillis();
c.add(Calendar.DAY_OF_MONTH, 1);
c.set(Calendar.HOUR_OF_DAY, 0);
c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
long millis = c.getTimeInMillis() - now+2000;
return (int)(millis/1000);
}
}
至此,本帖分享结束。