前提:首先我们的页面上有个图形验证码 然后有个短信验证码 有个点击发送按钮
每生成一次图形验证码都会放入缓存 后面发送短信验证码的请求 中的请求体必须包含它
其它短信发送相关bean见 :短信发送相关代码(使用了异步+线程池+http连接池 优化)
下面有俩接口 一个生成图形验证码 一个发送短信验证码
package net.xdclass.controller;
import com.google.code.kaptcha.Producer;
import lombok.extern.slf4j.Slf4j;
import net.xdclass.constant.RedisConstant;
import net.xdclass.controller.request.SendCodeRequest;
import net.xdclass.enums.BizCodeEnum;
import net.xdclass.enums.SendCodeEnum;
import net.xdclass.service.NotifyService;
import net.xdclass.util.CommonUtil;
import net.xdclass.util.JsonData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/api/v1/account/notify")
@Slf4j
public class NotifyController {
@Autowired
private Producer captchaProducer;
@Autowired
private NotifyService notifyService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 生成验证码
*
* @param request
* @param response
*/
@GetMapping("captcha")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {
String captchaText = captchaProducer.createText();
log.info("验证码内容:{}", captchaText);
//存储redis,配置过期时间
String captchaKey = getCaptchaKey(request);
redisTemplate.opsForValue().set(captchaKey, captchaText, RedisConstant.CAPTCHA_KEY_TIMEOUT, TimeUnit.MILLISECONDS);
BufferedImage bufferedImage = captchaProducer.createImage(captchaText);
try {
ServletOutputStream outputStream = response.getOutputStream();
ImageIO.write(bufferedImage, "jpg", outputStream);
outputStream.flush();
outputStream.close();
} catch (IOException e) {
log.error("获取流出错:{}", e.getMessage());
}
}
private String getCaptchaKey(HttpServletRequest request) {
String ip = CommonUtil.getIpAddr(request);
String userAgent = request.getHeader("User-Agent");
String captchaKey = "account-service:captcha:" + CommonUtil.MD5(ip + userAgent);
return captchaKey;
}
/**
* 测试发送验证码接口-主要是用于对比优化前后区别
*
* @return
*/
@PostMapping("send_code")
public JsonData sendCode(@RequestBody SendCodeRequest sendCodeRequest, HttpServletRequest request) {
String captchaKey = getCaptchaKey(request);
String cacheCaptchaValue = redisTemplate.opsForValue().get(captchaKey).toString();
String captcha = sendCodeRequest.getCaptcha();
String to = sendCodeRequest.getTo();
if (null != cacheCaptchaValue && null != captcha && captcha.equals(cacheCaptchaValue)) {
redisTemplate.delete(captchaKey);
JsonData result = notifyService.sendCode(SendCodeEnum.USER_REGISTER,to);
return result;
} else {
return JsonData.buildResult(BizCodeEnum.CODE_CAPTCHA_ERROR);
}
}
}
短信验证码请求体
@Data
public class SendCodeRequest {
private String captcha;
private String to;
}
为了防刷 短信验证码 60秒只能发一次 所以
以 前缀+to(to就是发送手机号) 为key 以验证码+时间戳为value
存入redis 来进行校验
package net.xdclass.service.impl;
import lombok.extern.slf4j.Slf4j;
import net.xdclass.component.SmsComponent;
import net.xdclass.config.SmsConfig;
import net.xdclass.constant.RedisConstant;
import net.xdclass.constant.RedisKey;
import net.xdclass.enums.BizCodeEnum;
import net.xdclass.enums.SendCodeEnum;
import net.xdclass.service.NotifyService;
import net.xdclass.util.CheckUtil;
import net.xdclass.util.CommonUtil;
import net.xdclass.util.JsonData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* 小滴课堂,愿景:让技术不再难学
*
* @Description
* @Author 二当家小D
* @Remark 有问题直接联系我,源码-笔记-技术交流群
* @Version 1.0
**/
@Service
@Slf4j
public class NotifyServiceImpl implements NotifyService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private SmsComponent smsComponent;
@Autowired
private SmsConfig smsConfig;
@Override
public JsonData sendCode(SendCodeEnum sendCodeEnum, String to) {
try {
//判断是否重复发送 以防刷 60秒内只能一次
String cacheKey = String.format(RedisKey.CHECK_CODE_KEY, SendCodeEnum.USER_REGISTER, to);
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
if (cacheValue != null) {
long leftTime = CommonUtil.getCurrentTimestamp() - Long.valueOf(cacheValue.split("_")[1]);
if (leftTime < 5 * 60 * 1000) {
return JsonData.buildResult(BizCodeEnum.CODE_LIMITED);
}
}
//若是超过一分钟则 重新把验证码存入缓存 并且发送
String sendCode = CommonUtil.getRandomCode(6);
cacheValue = sendCode + "_" + CommonUtil.getCurrentTimestamp();
redisTemplate.opsForValue().set(cacheKey, cacheValue, RedisConstant.CAPTCHA_KEY_TIMEOUT, TimeUnit.MILLISECONDS);
if (CheckUtil.isEmail(to)) {
//todo
return JsonData.buildSuccess();
} else if (CheckUtil.isPhone(to)) {
smsComponent.send(to, smsConfig.getTemplateId(), sendCode);
log.info("发送短信验证码成功");
return JsonData.buildSuccess();
}
} catch (Exception e) {
String errorMsg = "发送短信验证码失败";
log.info(errorMsg);
return JsonData.buildError(errorMsg);
}
return null;
}
}
相关枚举类 与常量类
public class RedisKey {
/**
* 验证码缓存key,第一个是类型,第二个是唯一标识比如手机号或者邮箱
*/
public static final String CHECK_CODE_KEY = "code:%s:%s";
}
public enum SendCodeEnum {
/**
* 用户注册
*/
USER_REGISTER;
}
import lombok.Getter;
public enum BizCodeEnum {
/**
* 短链分组
*/
GROUP_REPEAT(23001,"分组名重复"),
GROUP_OPER_FAIL(23503,"分组名操作失败"),
GROUP_NOT_EXIST(23404,"分组不存在"),
/**
*验证码
*/
CODE_TO_ERROR(240001,"接收号码不合规"),
CODE_LIMITED(240002,"验证码发送过快"),
CODE_ERROR(240003,"验证码错误"),
CODE_CAPTCHA_ERROR(240101,"图形验证码错误"),
/**
* 账号
*/
ACCOUNT_REPEAT(250001,"账号已经存在"),
ACCOUNT_UNREGISTER(250002,"账号不存在"),
ACCOUNT_PWD_ERROR(250003,"账号或者密码错误"),
ACCOUNT_UNLOGIN(250004,"账号未登录"),
/**
* 短链
*/
SHORT_LINK_NOT_EXIST(260404,"短链不存在"),
/**
* 订单
*/
ORDER_CONFIRM_PRICE_FAIL(280002,"创建订单-验价失败"),
ORDER_CONFIRM_REPEAT(280008,"订单恶意-重复提交"),
ORDER_CONFIRM_TOKEN_EQUAL_FAIL(280009,"订单令牌缺少"),
ORDER_CONFIRM_NOT_EXIST(280010,"订单不存在"),
/**
* 支付
*/
PAY_ORDER_FAIL(300001,"创建支付订单失败"),
PAY_ORDER_CALLBACK_SIGN_FAIL(300002,"支付订单回调验证签失败"),
PAY_ORDER_CALLBACK_NOT_SUCCESS(300003,"支付宝回调更新订单失败"),
PAY_ORDER_NOT_EXIST(300005,"订单不存在"),
PAY_ORDER_STATE_ERROR(300006,"订单状态不正常"),
PAY_ORDER_PAY_TIMEOUT(300007,"订单支付超时"),
/**
* 流控操作
*/
CONTROL_FLOW(500101,"限流控制"),
CONTROL_DEGRADE(500201,"降级控制"),
CONTROL_AUTH(500301,"认证控制"),
/**
* 流量包操作
*/
TRAFFIC_FREE_NOT_EXIST(600101,"免费流量包不存在,联系客服"),
TRAFFIC_REDUCE_FAIL(600102,"流量不足,扣减失败"),
TRAFFIC_EXCEPTION(600103,"流量包数据异常,用户无流量包"),
/**
* 通用操作码
*/
OPS_REPEAT(110001,"重复操作"),
OPS_NETWORK_ADDRESS_ERROR(110002,"网络地址错误"),
/**
* 文件相关
*/
FILE_UPLOAD_USER_IMG_FAIL(700101,"用户头像文件上传失败");
@Getter
private String message;
@Getter
private int code;
private BizCodeEnum(int code, String message){
this.code = code;
this.message = message;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class JsonData {
/**
* 状态码 0 表示成功
*/
private Integer code;
/**
* 数据
*/
private Object data;
/**
* 描述
*/
private String msg;
/**
* 获取远程调用数据
* 注意事项:
* 支持多单词下划线专驼峰(序列化和反序列化)
*
* @param typeReference
* @param
* @return
*/
public T getData(TypeReference typeReference){
return JSON.parseObject(JSON.toJSONString(data),typeReference);
}
/**
* 成功,不传入数据
* @return
*/
public static JsonData buildSuccess() {
return new JsonData(0, null, null);
}
/**
* 成功,传入数据
* @param data
* @return
*/
public static JsonData buildSuccess(Object data) {
return new JsonData(0, data, null);
}
/**
* 失败,传入描述信息
* @param msg
* @return
*/
public static JsonData buildError(String msg) {
return new JsonData(-1, null, msg);
}
/**
* 自定义状态码和错误信息
* @param code
* @param msg
* @return
*/
public static JsonData buildCodeAndMsg(int code, String msg) {
return new JsonData(code, null, msg);
}
/**
* 传入枚举,返回信息
* @param codeEnum
* @return
*/
public static JsonData buildResult(BizCodeEnum codeEnum){
return JsonData.buildCodeAndMsg(codeEnum.getCode(),codeEnum.getMessage());
}
}