在用户进行登录或者注册时,都会有输入验证码的相关操作,这就会涉及到发送验证码给用户的操作,发送方式大致分为两种:短信和邮箱,其中他们又有各种平台的发送方式,本次分别开发了阿里云的短信发送和QQ的邮箱发送,其他的类似。
图形验证码则是防止恶意刷短信次数设置的,主要是如何将生成的字符串转成图形验证码的过程。
(1)用户点击发送短信;
(2)后端编写发送短信的接口;
(3)如果请求次数过多,则需要一个创建图形验证码的接口;
(4)登录、注册。
/**
* 发送验证码到对应手机号或者邮箱
* @param loginName
* @return
*/
@PostMapping("/sendCode")
public ResponseResult<Boolean> sendCode(@MyRequestBody String loginName, @MyRequestBody String imageCode) {
// 判断是否需要检查图形验证码(同一登录名每天请求超过三次,则需要检查)
RAtomicLong atomicLong = redissonClient.getAtomicLong("sendCode-" + loginName);
if (atomicLong.isExists()) {
long sendNumber = atomicLong.get();
atomicLong.set(sendNumber+1);
} else {
// 设置初始请求次数和存在时长
atomicLong.set(1L);
atomicLong.expire(24, TimeUnit.HOURS);
}
if (atomicLong.get() > 3) {
// 判断图形验证码是否存在
if (imageCode == null){
throw new RuntimeException("图形验证码不能为空");
}
RBucket<String> bucket = redissonClient.getBucket("imageCode-" + loginName);
if (!bucket.isExists()) {
throw new RuntimeException("图形验证码已过期");
}
if (!imageCode.equals(bucket.get())) {
throw new RuntimeException("图形验证码输入错误");
}
}
Validate validate;
if (isEmail(loginName)) {
validate = validateHolder.getValidate(ValidateEnum.EMAIL.name());
} else {
validate = validateHolder.getValidate(ValidateEnum.ALIYUN_SMS.name());
}
return ResponseResult.success(validate.sendCode(loginName));
}
/**
* 用户注册apiKey
* @param loginName
* @param userName
* @return
*/
@PostMapping("/register")
public ResponseResult<ApiUser> register(
@MyRequestBody String loginName,
@MyRequestBody String userName,
@MyRequestBody String code) {
// 检查验证码是否正确
Validate validate;
if (isEmail(loginName)) {
validate = validateHolder.getValidate(ValidateEnum.EMAIL.name());
} else {
validate = validateHolder.getValidate(ValidateEnum.ALIYUN_SMS.name());
}
validate.verifyCode(loginName, code);
return ResponseResult.success(apiService.register(loginName, userName));
}
/**
* 获取图形验证码
* @return
*/
@PostMapping("/getImageCode")
public ResponseResult<String> getImageCode(@MyRequestBody String loginName) {
String imageCode = ImageCodeUtil.getCode();
RBucket<String> bucket = redissonClient.getBucket("imageCode-" + loginName);
bucket.set(imageCode);
// 设置超时时间
bucket.expire(2, TimeUnit.MINUTES);
// 获取图形验证码
BufferedImage codeImage = ImageCodeUtil.getCodeImage(imageCode);
// 转成base64
String base64Image = ImageCodeUtil.getBase64Image(codeImage);
return ResponseResult.success(base64Image);
}
private Boolean isEmail(String loginName) {
// 检查用户名是否符合邮箱的格式
String emailRegex = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
return loginName.matches(emailRegex);
}
/**
* 用户注册apiKey
* @param loginName
* @param userName
* @return
*/
@Override
@Transactional
public ApiUser register(String loginName, String userName) {
ApiUser user = apiUserMapper.selectOne(new QueryWrapper<ApiUser>().eq("login_name", loginName).eq("delete_flag",1));
if (user != null) {
throw new RuntimeException("用户已注册,请勿重复注册");
}
ApiUser apiUser = new ApiUser();
// 生成apiKey
String apiKey = UUID.randomUUID().toString();
apiUser.setApiKey(apiKey);
apiUser.setUserName(userName);
apiUser.setLoginName(loginName);
apiUser.setCreateTime(new Date());
apiUser.setUpdateTime(new Date());
// 将用户信息存入数据库
int insert = apiUserMapper.insert(apiUser);
if (insert < 1) {
throw new RuntimeException("注册失败,请重试!");
}
return apiUser;
}
}
/**
* ValidateHolder组件类
* @author CleloGauss
*/
@Component
public class ValidateHolder {
/**
* 用于存储验证器实例 key:名称 value:实例
*/
private Map<String, Validate> map = new HashMap<>();
@Resource
private AliyunValidate aliyunValidate;
@Resource
private EmailValidate emailValidate;
@PostConstruct
private void initialize() {
map.put(ValidateEnum.ALIYUN_SMS.name(), aliyunValidate);
map.put(ValidateEnum.EMAIL.name(), emailValidate);
}
/**
* 存储验证器
* @param validateName
* @param validate
*/
public void setValidate(String validateName, Validate validate) {
map.put(validateName, validate);
}
/**
* 获取不同验证器
* @param validateName
* @return
*/
public Validate getValidate(String validateName) {
return map.get(validateName);
}
}
/**
* 验证器(短信、邮箱)接口
* @author CleloGauss
*/
public interface Validate {
/**
* 发送验证码
* @return
*/
Boolean sendCode(String loginName);
/**
* 检验验证码
* @param code
* @return
*/
void verifyCode(String loginName, String code);
/**
* 生成验证码
* @return
*/
String getCode();
}
/**
* 验证器(阿里云)
* @author CleloGauss
*/
@Component("aliyunValidate")
public class AliyunValidate implements Validate {
// 下面的配置都可放入nacos配置文件中
public static String endpoint="dysmsapi.aliyuncs.com";
public static String regionId="cn-hangzhou";//机房信息,可以不用更改
public static String accessKey="";//需要修改
public static String accessSecret="";//需要修改
public static String sms_code="SMS_41635111";//需要修改
public static String signName = ""; // 签名
public static String templateCode = ""; // 模版编码
@Resource
private RedissonClient redissonClient;
/**
* 使用AK&SK初始化账号Client
* @param accessKey
* @param accessSecret
* @return
* @throws Exception
*/
private static Client createClient(String accessKey, String accessSecret) throws Exception {
Config config = new Config().setAccessKeyId(accessKey).setAccessKeySecret(accessSecret);
config.endpoint = endpoint;
config.setRegionId(regionId);
return new Client(config);
}
/**
* 发送验证码给用户
* @param phoneNumber
*/
@Override
public Boolean sendCode(String phoneNumber) {
Client client;
try {
client = createClient(accessKey, accessSecret);
} catch (Exception e) {
throw new RuntimeException("创建Client失败");
}
SendSmsRequest sendSmsRequest = new SendSmsRequest();
// 设置要发送的号码、验证码、签名
String code = getCode();
sendSmsRequest.setPhoneNumbers(phoneNumber)
.setSignName(signName)
.setTemplateCode(templateCode)
.setTemplateParam("{\"code\":"+code+"}");
try {
// 发送验证码
client.sendSmsWithOptions(sendSmsRequest, new RuntimeOptions());
} catch (Exception e) {
throw new RuntimeException("手机验证码发送失败");
}
return true;
}
@Override
public void verifyCode(String loginName, String code) {
// 判断验证码的正确性
RBucket<String> bucket = redissonClient.getBucket("verifyCode-" + loginName);
if (!bucket.isExists()) {
throw new RuntimeException("验证码已过期,请重新发送!");
}
if (!bucket.get().equals(code)){
throw new RuntimeException("验证码错误!");
}
}
/**
* 随机生成验证码(六位)
* @return
*/
@Override
public String getCode() {
Random random = new Random();
StringBuilder sb = new StringBuilder();
for(int i=0; i<6; i++){
int digit = random.nextInt(9) + 1;
sb.append(digit);
}
return sb.toString();
}
}
/**
* 验证器(邮箱)
* @author CleloGauss
*/
@Component("emailValidate")
public class EmailValidate implements Validate {
@Resource
private JavaMailSender javaMailSender;
@Resource
private RedissonClient redissonClient;
@Override
public Boolean sendCode(String loginName) {
String code = getCode();
// 将验证码放入缓存
RBucket<String> bucket = redissonClient.getBucket("verifyCode-" + loginName);
bucket.set(code);
// 设置超时时间
bucket.expire(5, TimeUnit.MINUTES);
// 邮件对象(邮件模板,根据自身业务修改)
SimpleMailMessage message = new SimpleMailMessage();
message.setSubject("**apiKey注册邮箱验证码");
message.setText("尊敬的用户您好!\n\n感谢您注册apiKey。\n\n尊敬的: " + loginName + "您的校验验证码为: " + code + ",有效期5分钟,请不要把验证码信息泄露给其他人,如非本人请勿操作");
message.setTo(loginName);
try {
// 对方看到的发送人(发件人的邮箱,根据实际业务进行修改,一般填写的是企业邮箱)
message.setFrom(new InternetAddress(MimeUtility.encodeText("dsajkl") + "<114***@qq.com>").toString());
// 发送邮件
javaMailSender.send(message);
} catch (Exception e) {
throw new RuntimeException("邮箱验证码发送失败");
}
return true;
}
@Override
public void verifyCode(String loginName, String code) {
// 判断验证码的正确性
RBucket<String> bucket = redissonClient.getBucket("verifyCode-" + loginName);
if (!bucket.isExists()) {
throw new RuntimeException("验证码已过期,请重新发送!");
}
if (!bucket.get().equals(code)){
throw new RuntimeException("验证码错误!");
}
}
/**
* 随机生成验证码(六位)
* @return
*/
@Override
public String getCode() {
Random random = new Random();
StringBuilder sb = new StringBuilder();
for(int i=0; i<6; i++){
int digit = random.nextInt(9) + 1;
sb.append(digit);
}
return sb.toString();
}
}
public enum ValidateEnum {
EMAIL("emailValidate"),
ALIYUN_SMS("aliyunValidate");
private String validate;
ValidateEnum(String validate) {
this.validate = validate;
}
}
/**
* 图形验证码工具类
* @author CleloGauss
*/
public class ImageCodeUtil {
/**
* 验证码来源
*/
private final static char[] charSet = {
'1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',
'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
};
/**
* 验证码长度
* 默认4个字符
*/
private final static int codeLen = 4;
/**
* 验证码图片字体大小
* 默认17
*/
private final static int fontsize = 21;
/**
* 验证码图片宽度
*/
private final static int width = (fontsize+1)*codeLen+10;
/**
* 验证码图片高度
*/
private final static int height = fontsize+12;
/**
* 干扰线条数
*/
private final static int disturbLine = 3;
/**
* 字体
*/
private final static String[] fontNames = new String[]{"黑体", "宋体", "Courier", "Arial", "Verdana", "Times", "Tahoma", "Georgia"};
private final static int[] fontStyles = new int[]{Font.BOLD, Font.ITALIC|Font.BOLD};
/**
* 获取验证码(4位)
* @return
*/
public static String getCode() {
Random random = new Random();
StringBuilder sb = new StringBuilder();
for(int i=0; i<codeLen; i++){
int index = random.nextInt(charSet.length);
sb.append(charSet[index]);
}
return sb.toString();
}
/**
* 获取图形验证码
* @param code
* @return
*/
public static BufferedImage getCodeImage(String code) {
// 创建验证码图片
BufferedImage codeImage = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
// 填充背景色
Graphics g = codeImage.getGraphics();
g.setColor(new Color(246, 240, 250));
g.fillRect(0,0,width,height);
// 添加干扰线条
drawDisturbLine(g);
// 在图片上画验证码
Random ran = new Random();
for (int i = 0; i < code.length(); i++) {
// 设置字体
g.setFont(new Font(
fontNames[ran.nextInt(fontNames.length)],
fontStyles[ran.nextInt(fontStyles.length)],
fontsize));
g.setColor(getRandomColor());
// 画验证码
g.drawString(code.charAt(i)+"", i*fontsize+10, fontsize+5);
}
// 释放相关资源
g.dispose();
return codeImage;
}
/**
* 将图形验证码转成base64
* @param codeImage
* @return
*/
public static String getBase64Image(BufferedImage codeImage) {
String base64Image;
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ImageIO.write(codeImage, "gif", bos);
byte[] imageBytes = bos.toByteArray();
base64Image = Base64.getEncoder().encodeToString(imageBytes);
} catch (IOException e) {
throw new RuntimeException("获取失败");
}
return "data:image/gif;base64,"+base64Image;
}
/**
* 为图片添加干扰线
* @param g
*/
private static void drawDisturbLine(Graphics g) {
Random ran = new Random();
for(int i = 0; i < disturbLine; i++){
int x1 = ran.nextInt(width);
int y1 = ran.nextInt(height);
int x2 = ran.nextInt(width);
int y2 = ran.nextInt(height);
g.setColor(getRandomColor());
//画干扰线
g.drawLine(x1, y1, x2, y2);
}
}
/**
* 返回随机颜色
* @return
*/
private static Color getRandomColor() {
Random ran = new Random();
return new Color(ran.nextInt(220), ran.nextInt(220), ran.nextInt(220));
}
}
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redisson.version}</version>
</dependency>
<!--阿里云短信发送-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.24</version>
</dependency>
<!--mail邮件发送-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
spring:
mail:
host: smtp.qq.com
username: 114****@qq.com
password: (自己去申请:QQ邮箱->设置->账号安全->申请授权码)
default-encoding: utf-8
properties:
mail:
smtp:
port: 465
ssl:
enable: true
required: true
timeout: 10000
connection-timeout: 10000
write-timeout: 10000
如果需要自己写其他平台的短信发送,可调用 ValidateHolder 中的 set 方法,编写自己的验证器,思路都一样。短信验证都需要去平台中开通短信服务,邮箱验证需要开启授权,图形验证码的核心都在工具类中。