自设计比较通用的短信验证码处理框架

[亡魂溺海] 提交于 2020-03-10 19:55:08

前言

使用手机的短信验证码来验证真实的用户身份,几乎是互联网网站以及App标配的功能之一。也就是开发这类系统,基本上都包含了这类功能点。本文将简单介绍开发一个较为通用的短信验证码处理框架的设计。基本上通过简易修改就可以用在自己的项目上。 以下是由底层一直往上到应用接口层的设计思路:

1,数据库设计:

数据库建表参考代码如下:

CREATE TABLE `sms_verify_code` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `phone` varchar(19) NOT NULL COMMENT '电话号码',
  `code` varchar(8) NOT NULL COMMENT '验证码',
  `datetime` datetime DEFAULT NULL COMMENT '发送时间',
  `usage_type` tinyint(4) DEFAULT '0' COMMENT '使用类型(辅助字段,一般用于区分验证码的用途,譬如普通身份验证短信,还是涉及金额的验证短信)',
  `valid` varchar(1) DEFAULT NULL COMMENT '目前是否生效',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=238 DEFAULT CHARSET=utf8mb4 COMMENT='手机短信验证码表';

一般数据库中要包含该短信需要发送给哪一个号码,验证码的值是多少,何时发送的,以及当前时刻该验证码是否还生效。另外还可以增加一个使用类型,来区分短信的用途,特别是如果系统涉及到银行金融或个人资金相关的类型时,需要加以区分和进行安全监控。

2,DAO层设计

底层数据访问层的参考代码如下:

[@Repository](https://my.oschina.net/u/3055569)
public interface SmsVerifyCodeRepository extends JpaRepository<SmsVerifyCode, Integer> {

	@Modifying
	@Query(value = "update sms_verify_code set valid = 'N'  where phone=?1 and valid = 'Y' and usage_type = ?2", nativeQuery = true)
	int changeOldCodeStatusByUsageType(String phoneNum, int usageType);

	SmsVerifyCode findByPhoneAndCodeAndUsageTypeAndValid(String phoneNum, String code, int usageType, String valid);


	@Query(value = "select count(1) from sms_verify_code where phone = ?1 and datetime > ?2 and usage_type = ?3 and valid = 'Y'", nativeQuery = true)
	int findInUseCountByPhoneAndLowLimitTime(String phone, String lowLimitTime, int usageType);

	@Modifying
	@Transactional
	@Query(value = "delete from sms_verify_code where datetime < ?1", nativeQuery = true)
	int deleteExpireCodes(String lowLimitTime);

	@Modifying
	@Transactional
	@Query(value = "update sms_verify_code set valid = 'N'  where datetime < ?1", nativeQuery = true)
	int dealWithExpireCodes(String lowLimitTime);

}

Spring boot中的Repository层,实际上与DAO层基本相同,其包含了简单的能与数据库进行短信数据访问和存储功能,可以使用特定命名的方法或者原生SQL与数据库进行数据交互。

3,service层设计
先设计一个接口,然后再设计一个该接口的实现。接口的参考代码如下:

public interface SmsVerifyCodeService {

	boolean changeCodeToObsolete(String phoneNum, int usageType);

	boolean addNewCode(String phoneNum, String code, int usageType) ;

	boolean checkThePhoneNumAndCodeMatchWithUsageType(String phoneNum, String code, int usageType);

	boolean allowInsertByIntervalSecondAndUsageType(String phoneNum, int interval, int usageType);

	boolean deleteExpireCodes(int timeInterval);

	boolean dealWithExpireCodes(int timeInterval);

}

该接口包含了6个方法,基本上能够完成短信验证的所有功能。其中deleteExpireCodes( )方法是硬删除,而dealWithExpireCodes( )方法是软删除,实际看自己项目的需要,该类方法可以放入Quartz等任务调度中辅助完成,最后一小节讲有该内容补充。

接口的实现的参考代码如下:

@Service
public class SmsVerifyCodeServiceImpl implements SmsVerifyCodeService{

	@Autowired
	SmsVerifyCodeRepository smsVerifyCodeRepository;

//    /**修改某电话号码对应的旧验证码的状态,一般置为无效*/
	@Transactional
	public boolean changeCodeToObsolete(String phoneNum, int usageType){
		smsVerifyCodeRepository.changeOldCodeStatusByUsageType(phoneNum, usageType);
		return true;
	}

	/**插入一条新的验证码*/
	@Transactional
	public boolean addNewCode(String phoneNum, String code, int usageType){
		SmsVerifyCode verifyCode = new SmsVerifyCode();
		verifyCode.setPhone(phoneNum);
		verifyCode.setCode(code);
		verifyCode.setDatetime(new Date());
		verifyCode.setValid("Y");
		verifyCode.setUsageType(usageType);

		smsVerifyCodeRepository.changeOldCodeStatusByUsageType(phoneNum, usageType);//强制将该号码的之前的验证码设置失效
		smsVerifyCodeRepository.save(verifyCode);
		return true;
	}


	/**对电话号码和验证码进行验证*/
	public boolean checkThePhoneNumAndCodeMatchWithUsageType(String phoneNum, String code, int usageType){
		boolean returnFlag = false;

		SmsVerifyCode codeEntiry = smsVerifyCodeRepository.findByPhoneAndCodeAndUsageTypeAndValid(phoneNum, code, usageType,"Y");

		if(codeEntiry != null) {
			returnFlag = true;
		}

		return returnFlag;
	}

	/**
	 * 在某时间间隔之内,是否允许发送(插入)短信验证码,可防止恶意刷短信
	 * @param phoneNum
	 * @param interval
	 * @return
	 */
	public boolean allowInsertByIntervalSecondAndUsageType(String phoneNum, int interval, int usageType){
		int count  = 0;
		boolean rtnFlag = true;

		Long lowLimitTimeStamp = Instant.now().toEpochMilli() - interval * 1000;
		String strLowLimitTime = DateUtils.formatDateToString(new Date(lowLimitTimeStamp), "yyyy-MM-dd HH:mm:ss");
		count = smsVerifyCodeRepository.findInUseCountByPhoneAndLowLimitTime(phoneNum, strLowLimitTime, usageType);
		if( count > 0 ){
			rtnFlag = false;
		}
		return rtnFlag;
	}

	@Override
	public boolean deleteExpireCodes(int timeInterval) {
		Long lowLimitTimeStamp = Instant.now().toEpochMilli() - timeInterval * 1000;
		String strLowLimitTime = DateUtils.formatDateToString(new Date(lowLimitTimeStamp), "yyyy-MM-dd HH:mm:ss");
		smsVerifyCodeRepository.deleteExpireCodes(strLowLimitTime);
		return true;
	}

	@Override
	public boolean dealWithExpireCodes(int timeInterval) {
		Long lowLimitTimeStamp = Instant.now().toEpochMilli() - timeInterval * 1000;
		String strLowLimitTime = DateUtils.formatDateToString(new Date(lowLimitTimeStamp), "yyyy-MM-dd HH:mm:ss");
		smsVerifyCodeRepository.dealWithExpireCodes(strLowLimitTime);
		return true;
	}

}

4,应用接口层设计
应用接口层,一般由Controller来完成接口的发布操作,一般是服务端与前端进行数据交互和参数拼装处理的最外层。参考代码如下:

@RestController
@RequestMapping("/")
@CrossOrigin
@Slf4j
public class VerifyCodeController {
	/**
	 * 图片验证码,短信验证码相关逻辑都会放入该controller
	 *
	 * */

	@Value("${sms.sender.status}")
	private String smsSenderStatus;

	@Value("${user.tmp.verify.save.addr}")
	private String userTmpVerifySaveAddr;

	@Value("${sms.service.app.address}")
	private String smsServiceAppAddress;

	@Value("${sms.interface.name}")
	private String smsInterfaceName;

	@Value("${sms.account}")
	private String smsAccount;

	@Value("${sms.pswd}")
	private String smsPWD;


	@Autowired
	SmsVerifyCodeService smsVerifyCodeService;

	@Autowired
	private RedisTemplate redisTemplate;

	@PostMapping(value="/sendVerifyCodeNew", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,  produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
	public ResponseBean sendVerifyCodeNew(@RequestBody HashMap<String, String> mapParam, HttpSession httpSession){
		ResponseBean responseBean = null;

		String smsInterfaceServiceURL = "http://" + smsServiceAppAddress + "/" + smsInterfaceName;



		String pin = "";
		if (mapParam.containsKey("pin")){
			pin = mapParam.get("pin");
		}

		........//省略部分代码

		if (smsSenderStatus == null || smsSenderStatus.equals("")){
			smsSenderStatus = "open";
		}

		Map<String, String> rtnListMap = new HashMap<String, String>();

		//下一次发短信允许的时间间隔,可模拟用户点击发送短信验证码按钮验证
		int interval = 60;
		String phoneNum = "";
		if (mapParam.containsKey("phone")){
			phoneNum = mapParam.get("phone");
		}

		......//省略部分代码


		//后端级别的允许短信验证码发送的检测,建议前端也要有相关发送短信的间隔检验。
		boolean allowSendFlag = smsVerifyCodeService.allowInsertByIntervalSecondAndUsageType(phoneNum, interval, 0);

		String smsCode = "";
		//生成短信验证码
		if (allowSendFlag == true){
			smsCode = VerifyCodeUtils.getSmsVerifyCodeBySize(6);
			//调用短信网关接口,发送验证码
			if (smsSenderStatus.equals("open")){//真实发送短信验证码的情况
				RestTemplate restTemplate = new RestTemplate();

				HttpHeaders httpHeaders = new HttpHeaders();
				httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

				MultiValueMap<String, String> mapParams = new LinkedMultiValueMap<String, String>();
				mapParams.add("account", smsAccount);
				mapParams.add("pswd", smsPWD);

				mapParams.add("msg", "您好,您的验证码是{$var}");
				String params = "" + phoneNum + "," + smsCode;
				mapParams.add("params", params);


				HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(mapParams, httpHeaders);

				log.info("发送给短信网关的数据为:{}" + mapParam.toString());

				try {
					ResponseEntity<String> response = restTemplate.exchange(smsInterfaceServiceURL, HttpMethod.POST, request, String.class);
					String rtnString = response.getBody();
					log.info(rtnString);
				}catch(Exception e){
					log.warn("数据:{},未能发送给短信网关,原因为连接超时。", mapParam.toString());
				}

			} else{//以json接口返回来模拟发送短信的情况
				rtnListMap.put("msgPin", smsCode);//实际上验证码是通过短信发送给用户的,不存在接口直接返回该值,以后需去除该行。
				responseBean = ResponseBeanUtils.buildSuccessBean();
				responseBean.setData(rtnListMap);
			}
			smsVerifyCodeService.addNewCode(phoneNum, smsCode, 0);
		}else{
			//告知在约定的时间间隔中不能再次发送短信验证码,请耐心等待
			responseBean = ResponseBeanUtils.buildErrorBean("验证码发送间隔时间为1分钟,请耐心等待");
		}
		return responseBean;
	}




	@PostMapping(value="/checkVerifyCode", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,  produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
	public ResponseBean checkVerifyCode(@RequestBody HashMap<String, String> mapParam){

		ResponseBean responseBean = null;
		HashMap<String, String> rtnListMap = new HashMap<String, String>();

		//验证短信验证码
		String phoneNum = mapParam.get("phone");
		String smsCode = mapParam.get("msgPin");


		boolean rtnFlag = smsVerifyCodeService.checkThePhoneNumAndCodeMatchWithUsageType(phoneNum, smsCode, 0);

		if (rtnFlag == true){
			//短信验证码正确,可以进行相关的登录或注册逻辑

			//完成登录或者注册逻辑,需要将该条短信验证码设置为false。
			smsVerifyCodeService.changeCodeToObsolete(phoneNum, 0);
			responseBean = ResponseBeanUtils.buildSuccessBean();
		}else{

			responseBean = ResponseBeanUtils.buildErrorBean("短信验证码错误");
		}

		rtnListMap.put("valid", String.valueOf(rtnFlag));
		responseBean.setData(rtnListMap);

		return responseBean;
	}

	@GetMapping(value="/getImgVerifyCode")
	public void getImgVerifyCode(HttpServletRequest request, HttpServletResponse response){
		// 设置响应的类型格式为图片格式
		response.setContentType("image/jpeg");
		// 禁止图像缓存。
		response.setHeader("Pragma", "no-cache");
		response.setHeader("Cache-Control", "no-cache");
		response.setDateHeader("Expires", 0);

		File dir = new File(userTmpVerifySaveAddr);
		int w = 100, h = 40;
		//生成验证码测试
		String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
		//对于verifyCode,最终我们会按该值生产一张干扰化的验证码图片给到前端,并且后续前端需要回传一个用户输入的验证码
		//与该图片对应的verifyCode进行对比,为了做多one-to-one correspondence,我们可以设置图片id与verifyCode的值对应并
		//放入到session中,如String类型的key-value --> codeImageId = 某verifyCode 这样,并且设置一个expire时间
		//前后端传送图片验证码的时候,都需同时带上codeImageId。
		File file = new File(dir, verifyCode + ".jpg");
		HttpSession session = request.getSession(true);
		//session.setMaxInactiveInterval(60 * 1000);
		session.setAttribute(ExpressionConstant.USER_TMP_VERIFY_CODE_SAVE, verifyCode );
		try {
			VerifyCodeUtils.outputImage(w, h, response.getOutputStream(), verifyCode);
		} catch (IOException e) {
			log.warn("图片验证码转换错误");
		}


	}

}

5,配置文件简介
配置文件中一般包含了需要注入到Controller中的可配置参数信息,参考配置如下:

#sms interface params
sms.service.app.address=XXXXXX
sms.interface.name=sms/HttpVarSM
sms.account=XXXXXX
sms.pwd=XXXXXX

6,使用Quartz等任务调度器完善短信数据的变更
熟悉Quartz的可以书写Quartz任务调度进行短信的数据变更,譬如过时短信的删除或标记失效等等。

Quartz Job的参考代码如下:

@Component
@Slf4j
public class DelExpireCodeJob extends QuartzJobBean {

	@Autowired
	SmsVerifyCodeService smsVerifyCodeService;


	@Override
	protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
		try{
//            smsVerifyCodeService.deleteExpireCodes(60*60*24*180);
			smsVerifyCodeService.dealWithExpireCodes(60*60*24*180);
			log.info("短信验证码过期数据自动清除完成。");
		}catch(Exception e){
			log.info("短信验证码自动清除程序运行有误!");
			log.warn(e.toString());
		}
	}
}

JobDetail以及触发器配置的参考代码如下:

@Configuration
public class DelExpireCodeQuartzConfig {
	@Bean
	public JobDetail delExpireCodeJobDetail() {
		return JobBuilder.newJob(DelExpireCodeJob.class).withIdentity("delete_expire_sms_code","group1").storeDurably().build();
	}

	@Bean
	public Trigger delExpireCodeJobTrigger() {
		//每隔10分处理一次
		CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0 0/10 * * * ?");
		return TriggerBuilder.newTrigger().forJob(delExpireCodeJobDetail())
				.withIdentity("delete_expire_sms_code","group1")
				.withSchedule(scheduleBuilder)
				.build();
	}
}

这样基本就设计完成了一个比较通用的短信验证框架了。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!