参考文章:邮箱验证码登录的简易实现
1. 引入依赖
XML
<!-- 1.邮箱依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>2.7.2</version>
</dependency>
<!-- 2.模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- 3.validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 4.HuTool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.5</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 说明
- **
2.模板引擎**是用于美化发送的邮件样式 - 本邮件验证登录以qq邮箱作为发送箱,因此需要引入qq邮箱官网的依赖支持**
3.validation**
- **
2. 配置
XML
spring:
mail:
host: smtp.qq.com
username: # 用来发送邮件的邮箱账号
password: # 授权码
default-encoding: UTF-8
redis:
database: 1
host: 127.0.0.1
port: 6379
password: # 密码
- 说明
- 需要登录自己的邮箱账号,在
设置-账户-POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务那开启POP3/SMTP 服务,同时获取授权码 - 引入 redis 数据库是为了保存用户的验证码和校验通过后的状态
- 需要登录自己的邮箱账号,在
3. 使用示例
a. util,用于生成验证码
- 文件名:VerificationCodeUtil.java
- 存放位置:src/main/java/com/yu/cloudpicturebackend/utils/VerificationCodeUtil.java
java
package com.yu.cloudpicturebackend.utils;
import java.util.Random;
public class VerificationCodeUtil {
/**
* 随机生成验证码
*
* @param length 长度为 4 位或者 6 位
* @return
*/
public static Integer getVerificationCode(int length) {
Integer code = null;
if (length == 4) {
code = new Random().nextInt(9999);//生成随机数,最大为9999
if (code < 1000) {
code = code + 1000;//保证随机数为4位数字
}
} else if (length == 6) {
code = new Random().nextInt(999999);//生成随机数,最大为999999
if (code < 100000) {
code = code + 100000;//保证随机数为6位数字
}
} else {
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code;
}
/**
* 随机生成指定长度字符串验证码
*
* @param length 长度
* @return
*/
public static String generateValidateCode4String(int length) {
Random rdm = new Random();
String hash = Integer.toHexString(rdm.nextInt());
return hash.substring(0, length);
}
}
b. controller层
- 文件名:
AuthController.java 存放位置:src/main/java/com/yu/cloudpicturebackend/controller/AuthController.java
java
package com.yu.cloudpicturebackend.controller;
import cn.hutool.core.util.ObjUtil;
import com.yu.cloudpicturebackend.common.ResultUtils;
import com.yu.cloudpicturebackend.exception.BaseResponse;
import com.yu.cloudpicturebackend.exception.BusinessException;
import com.yu.cloudpicturebackend.exception.ErrorCode;
import com.yu.cloudpicturebackend.model.dto.SendEmailRequest;
import com.yu.cloudpicturebackend.model.dto.VerifyEmailRequest;
import com.yu.cloudpicturebackend.service.AuthService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/auth")
@Api(tags = "授权接口")
public class AuthController {
@Autowired
private AuthService authService;
@PostMapping("/email/send")
@ApiOperation("发送邮箱验证码")
public BaseResponse<String> sendEmail(@RequestBody SendEmailRequest sendEmailRequest) {
if (ObjUtil.isEmpty(sendEmailRequest)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
authService.handleSend(sendEmailRequest);
return ResultUtils.success("验证码发送成功");
}
@PostMapping("/email/verify")
@ApiOperation("校验邮箱验证码")
public BaseResponse<String> verifyCode(@RequestBody VerifyEmailRequest verifyEmailRequest) {
if (ObjUtil.isEmpty(verifyEmailRequest)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 校验邮箱验证码
authService.verifyCode(verifyEmailRequest);
return ResultUtils.success("校验成功");
}
}
c. service层
- 文件名:
AuthService.java 存放位置:src/main/java/com/yu/cloudpicturebackend/service/AuthService.java
1. 发送邮箱验证码
java
package com.yu.cloudpicturebackend.service;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.mail.Mail;
import cn.hutool.extra.mail.MailAccount;
import cn.hutool.extra.template.Template;
import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateUtil;
import com.yu.cloudpicturebackend.exception.BusinessException;
import com.yu.cloudpicturebackend.exception.ErrorCode;
import com.yu.cloudpicturebackend.model.dto.SendEmailRequest;
import com.yu.cloudpicturebackend.model.dto.VerifyEmailRequest;
import com.yu.cloudpicturebackend.model.entity.EmailRo;
import com.yu.cloudpicturebackend.utils.VerificationCodeUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
@Slf4j
public class AuthService {
//获取配置文件的信息
@Value("${spring.mail.host}")
private String host;
@Value("${spring.mail.username}")
private String username;
@Value("${spring.mail.password}")
private String password;
// springboot 整合 redis
@Resource
private StringRedisTemplate stringRedisTemplate;
public final String SEND_KEY_PRE = "email:send:";
public final String STATUS_KEY_PRE = "email:verified:";
/**
* 处理发送邮件验证码逻辑
*
* @param sendEmailRequest
*/
public void handleSend(SendEmailRequest sendEmailRequest) {
String email = sendEmailRequest.getEmail();
// 校验邮箱格式
isValidateEmail(email);
if (StrUtil.isBlank(email)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "邮箱不能为空");
}
// 获取发送邮箱验证码的 HTML 模板
TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("templates",
TemplateConfig.ResourceMode.CLASSPATH));
Template template = engine.getTemplate("email-code.ftl");
// 获取验证码
String code = VerificationCodeUtil.getVerificationCode(6).toString();
sendEmail(new EmailRo(email, "邮箱验证码登录", template.render(Dict.create().set("code", code)), code));
}
/**
* 发送邮箱验证码
*
* @param emailRo
*/
public void sendEmail(EmailRo emailRo) {
// 设置
MailAccount account = new MailAccount();
account.setHost(host);
// 设置发送人信息
account.setFrom("鱼籽云图库" + "<" + username + ">");
// 设置发送人名称
account.setUser(username);
// 设置发送授权码
account.setPass(password);
account.setAuth(true);
// ssl方式发送
account.setSslEnable(true);
// 使用安全连接
account.setStarttlsEnable(true);
// 发送邮件
try {
Mail.create(account)
.setTos(emailRo.getEmail())
.setTitle(emailRo.getSubject())
.setContent(emailRo.getContent())
.setHtml(true)
// 关闭session
.setUseGlobalSession(false)
.send();
// 将验证码保存到 redis
stringRedisTemplate.opsForValue().set(SEND_KEY_PRE + emailRo.getEmail(), emailRo.getCode(), 120, TimeUnit.SECONDS);
} catch (Exception e) {
log.error(e.toString());
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "邮件发送失败");
}
}
/**
* 校验邮箱格式
*
* @param email
* @return
*/
public void isValidateEmail(String email) {
// 只允许字母、数字和 _+&*- 在本地部分
// 要求域名部分有2-7个字母的顶级域名
// 更简单但可能排除一些有效的邮箱格式
String regex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(email);
if (!matcher.matches()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "邮箱格式不正确");
}
}
}
2. 校验邮箱验证码
java
/**
* 校验邮件验证码
*
* @param verifyEmailRequest
* @return
*/
public void verifyCode(VerifyEmailRequest verifyEmailRequest) {
String email = verifyEmailRequest.getEmail();
String code = verifyEmailRequest.getCode();
if (StrUtil.isBlank(code)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "验证码不能为空");
}
// 校验邮箱格式
isValidateEmail(email);
// 从 Redis 获取存储的验证码
String storedCode = stringRedisTemplate.opsForValue().get(email);
if (storedCode == null || !code.equals(storedCode)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "验证码错误");
}
// 校验通过,设置验证状态(有效期 10 分钟)
String statusKey = STATUS_KEY_PRE + verifyEmailRequest.getEmail();
stringRedisTemplate.opsForValue().set(statusKey, "true", 10, TimeUnit.MINUTES);
}
d. 模板文件
- 文件名:
email-code.ftl - 存放位置:src/main/resources/templates/email-code.ftl
html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>邮箱验证码</title>
<style type="text/css">
/* 重置样式 */
body, p, div {
margin: 0;
padding: 0;
}
body {
width: 100% !important;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
margin: 0;
padding: 0;
font-family: 'Microsoft YaHei', SimSun, Arial, sans-serif;
line-height: 1.6;
color: #333333;
background-color: #f6f9fc;
}
/* 外层容器 */
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
/* 头部样式 */
.email-header {
background: linear-gradient(135deg, #4A6EE0 0%, #6A11CB 100%);
color: white;
padding: 25px 30px;
text-align: center;
}
.email-header h1 {
font-size: 24px;
margin: 0;
font-weight: 600;
}
/* 内容区域 */
.email-content {
padding: 30px;
}
.greeting {
font-size: 16px;
margin-bottom: 20px;
color: #555;
}
.instruction {
font-size: 15px;
line-height: 1.7;
margin-bottom: 25px;
color: #666;
}
/* 验证码样式 */
.verification-code {
background: #f8f9fa;
border-left: 4px solid #4A6EE0;
padding: 20px;
margin: 25px 0;
text-align: center;
border-radius: 0 6px 6px 0;
}
.code-text {
font-size: 48px;
font-weight: 700;
letter-spacing: 5px;
color: #4A6EE0;
margin: 10px 0;
font-family: 'Courier New', monospace;
}
.code-expiry {
font-size: 14px;
color: #888;
margin-top: 10px;
}
/* 提示信息 */
.tips {
background-color: #f0f7ff;
border-radius: 6px;
padding: 15px;
margin: 25px 0;
font-size: 14px;
color: #555;
border-left: 3px solid #4A6EE0;
}
.tips p {
margin: 5px 0;
}
/* 底部样式 */
.email-footer {
background-color: #f8f9fa;
padding: 20px 30px;
text-align: center;
font-size: 13px;
color: #888;
border-top: 1px solid #eaeaea;
}
.company-info {
margin-bottom: 10px;
}
.support-info {
margin-top: 10px;
font-size: 12px;
}
/* 响应式设计 */
@media only screen and (max-width: 600px) {
.email-container {
width: 100% !important;
border-radius: 0;
}
.email-content, .email-header, .email-footer {
padding: 20px !important;
}
.code-text {
font-size: 28px !important;
letter-spacing: 3px !important;
}
}
</style>
</head>
<body>
<center>
<table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="#f6f9fc">
<tr>
<td align="center" valign="top">
<table class="email-container" border="0" cellpadding="0" cellspacing="0" width="600">
<!-- 头部 -->
<tr>
<td class="email-header">
<h1>邮箱验证</h1>
</td>
</tr>
<!-- 内容区域 -->
<tr>
<td class="email-content">
<p class="greeting">尊敬的用户,您好:</p>
<p class="instruction">您正在申请邮箱验证,为了确保是您本人操作,请在验证页面输入以下验证码:</p>
<div class="verification-code">
<div class="code-text">${code}</div>
<p class="code-expiry">验证码有效期为15分钟,请尽快使用</p>
</div>
<div class="tips">
<p><strong>温馨提示:</strong></p>
<p>• 请勿将验证码告知他人,工作人员不会向您索要验证码</p>
<p>• 如非本人操作,请忽略此邮件</p>
<p>• 如有疑问,请联系我们的客服团队</p>
</div>
<p class="instruction">感谢您使用我们的服务!</p>
</td>
</tr>
<!-- 底部 -->
<tr>
<td class="email-footer">
<div class="company-info">鱼籽科技有限公司</div>
<div>此为系统邮件,请勿直接回复</div>
<div class="support-info">如需帮助,请联系客服邮箱:1319764245@qq.com</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</center>
</body>
</html>
e. dto
- 文件名:EmailRequest.java
- 存放位置:src/main/java/com/yu/cloudpicturebackend/model/dto/EmailRequest.java
java
package com.yu.cloudpicturebackend.model.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class EmailRequest implements Serializable {
private static final long serialVersionUID = 3662164451473225507L;
/**
* 邮箱
*/
private String email;
}
f. model
- 文件名:EmailRo
- 存放位置:src/main/java/com/yu/cloudpicturebackend/model/entity/EmailRo.java
java
package com.yu.cloudpicturebackend.model.entity;
import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 邮件请求对象
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmailRo implements Serializable {
private static final long serialVersionUID = -8620593378368489081L;
/**
* 收件人邮箱地址
*/
private String email;
/**
* 邮件主题
*/
private String subject;
/**
* 邮件内容
*/
private String content;
/**
* 验证码
*/
private Integer code;
}