为了响应公司项目的特定需求,增强用户体验与安全性,集成手机短信验证码登录功能至基于若依微服务框架开发的应用中,故创作此篇为未来类似项目提供了可借鉴的实施范例。
1.设计思路
大致可分为这几个阶段:生成验证码并存储至redis缓存中,发送验证码,用户登陆验证
2.发送手机验证码接口
/**
* 发送手机验证码
* @param phoneNumber
* @return
*/
@GetMapping("/code/{phoneNumber}")
public AjaxResult sendCode(@PathVariable String phoneNumber) {
// 从Redis中获取最后一次发送验证码的时间戳
String lastSentTimeStr = redisService.getCacheObject(phoneNumber + "lastSendTime");
if (lastSentTimeStr != null) {
long lastSentTime = Long.parseLong(lastSentTimeStr);
// 计算当前时间与上次发送时间的时间差
long timeDiff = Duration.between(Instant.ofEpochMilli(lastSentTime), Instant.now()).toMillis();
// 如果时间差小于60秒,则返回错误
if (timeDiff < 60_000) {
return AjaxResult.error("您的短信发送过于频繁,请60s后再试!");
}
}
//生成6位随机验证码
Random randObj = new Random();
String smsCode = Integer.toString(100000 + randObj.nextInt(900000));
//发送短信
boolean isSend = sysSmsApiService.send(phoneNumber,smsCode);
if(!isSend){
return AjaxResult.error("短信发送失败!");
}
// 更新Redis中该手机号最后发送验证码的时间戳
redisService.setCacheObject(phoneNumber + "lastSendTime", String.valueOf(Instant.now().toEpochMilli()), 60L, TimeUnit.SECONDS);
//将验证码保存至redis缓存中,设置有限期为5分钟
redisService.setCacheObject(phoneNumber,smsCode,5L,TimeUnit.MINUTES);
return AjaxResult.success();
}
3.发送手机验证码接口
@Value("${tax.url}")
private String url;
@Value("${tax.key}")
private String key;
@Value("${tax.vipara}")
private String vipara;
/**
* 发送短信
* @param phoneNumber
* @param smsCode
* @return
*/
@Override
public boolean send(String phoneNumber, String smsCode) {
try {
SendtoVo sendtoVo = new SendtoVo();
sendtoVo.setSjhm(phoneNumber);
String content = "短信模板"; //自己根据业务需求编写
sendtoVo.setContent(content);
HashMap<Object, Object> map = new HashMap<>();
map.put("*******", "********");
map.put("data", JSON.toJSONString(sendtoVo));
Map result = doPostUtils.doPost(url, map, key, vipara); //此处传入小程序配置参数
// System.out.println("短信发送返回结果======================:" + result);
if (result != null) {
return true;
} else {
return false;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
4.post请求工具类
/**
* 向指定 URL 发送POST方法的请求
*
* @param httpUrl
* 发送请求的 URL
* @param map
* 请求参数是json
* @param key
* key
* @return 所代表远程资源的响应结果
*/
public static Map doPost(String httpUrl, Map map,String key,String vipara) {
log.info(JSON.toJSONString(map));
map.put("data",Base64Utils.JsonToBase64((String) map.get("data")));
String jsonString = JSON.toJSONString(map);
String Aesdata = AesUtils.encrypt(jsonString, key,vipara);
HttpURLConnection connection = null;
InputStream is = null;
OutputStream os = null;
BufferedReader br = null;
String result = null;
try {
URL url = new URL(httpUrl);
// 通过远程url连接对象打开连接
connection = (HttpURLConnection) url.openConnection();
// 设置连接请求方式
connection.setRequestMethod("POST");
// 设置连接主机服务器超时时间:15000毫秒
connection.setConnectTimeout(15000);
// 设置读取主机服务器返回数据超时时间:60000毫秒
connection.setReadTimeout(60000);
// 默认值为:false,当向远程服务器传送数据/写数据时,需要设置为true
connection.setDoOutput(true);
// 默认值为:true,当前向远程服务读取数据时,设置为true,该参数可有可无
connection.setDoInput(true);
// 设置传入参数的格式:请求参数应该是 name1=value1&name2=value2 的形式。
connection.setRequestProperty("Content-Type", "application/json");
// 设置鉴权信息:Authorization: Bearer da3efcbf-0845-4fe3-8aba-ee040be542c0
// connection.setRequestProperty("Authorization", "Bearer
// da3efcbf-0845-4fe3-8aba-ee040be542c0");
// 通过连接对象获取一个输出流
os = connection.getOutputStream();
// 通过输出流对象将参数写出去/传输出去,它是通过字节数组写出的
os.write(Aesdata.getBytes());
// 通过连接对象获取一个输入流,向远程读取
if (connection.getResponseCode() == 200) {
is = connection.getInputStream();
// 对输入流对象进行包装:charset根据工作项目组的要求来设置
br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
StringBuffer sbf = new StringBuffer();
String temp = null;
// 循环遍历一行一行读取数据
while ((temp = br.readLine()) != null) {
sbf.append(temp);
sbf.append("\r\n");
}
result = sbf.toString();
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != os) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 断开与远程地址url的连接
connection.disconnect();
}
String result1 = AesUtils.decrypt(result, key,vipara);
Map map1 = JSON.parseObject(result1, Map.class);
String data2 = (String) map1.get("data");
if ("0".equals(map1.get("code").toString())){
String s = Base64Utils.Base64ToJson(data2);
map1.put("data",s);
log.info(map1.toString());
return map1;
}
return null;
}
5.登录校验接口
/**
* 手机短信登录
* @param phoneNumber
* @param smsCode
* @return
*/
public LoginUser smsLogin(String phoneNumber, String smsCode) {
// 手机号或验证码为空 错误
if (StringUtils.isAnyEmpty(phoneNumber, smsCode)) {
recordLogininfor(phoneNumber, Constants.LOGIN_FAIL, "手机号/验证码必须填写");
throw new ServiceException("手机号/验证码必须填写");
}
// 手机号输入错误
if (phoneNumber.length() != UserConstants.PHONE_NUMBER_LENGTH){
recordLogininfor(phoneNumber, Constants.LOGIN_FAIL, "手机号输入错误");
throw new ServiceException("手机号输入错误,请检查");
}
// 验证码输入错误
if (smsCode.length() != UserConstants.CODE_LENGTH){
recordLogininfor(phoneNumber, Constants.LOGIN_FAIL, "验证码输入错误");
throw new ServiceException("验证码输入错误,请检查");
}
//通过手机号查询用户信息
R<LoginUser> userResult = remoteUserService.getUserInfoByPhoneNumber(phoneNumber, SecurityConstants.INNER);
if (R.FAIL == userResult.getCode()) {
throw new ServiceException(userResult.getMsg());
}
if (StringUtils.isNull(userResult.getData())) {
recordLogininfor(phoneNumber, Constants.LOGIN_FAIL, "登录手机号不存在");
throw new ServiceException("登录手机号:" + phoneNumber + " 不存在");
}
LoginUser userInfo = userResult.getData();
//在redis中获取验证码
String cacheCode = redisService.getCacheObject(phoneNumber);
if(cacheCode == null || ObjectUtils.isEmpty(cacheCode)) {
recordLogininfor(phoneNumber, Constants.LOGIN_FAIL, "验证码不存在或已过期");
throw new ServiceException("验证码不存在或已过期");
}
if(cacheCode.equals(smsCode)){
//验证码正确,可删除验证码
redisService.deleteObject(phoneNumber);
recordLogininfor(phoneNumber, Constants.LOGIN_SUCCESS, "登录成功");
} else {
recordLogininfor(phoneNumber, Constants.LOGIN_FAIL, "验证码输入有误");
throw new ServiceException("验证码输入有误");
}
return userInfo;
}
6.nacos配置请求白名单
7.nacos配置短信服务密钥
8.获取手机验证码测试
9.用户登录测试