前言
对于用户模块来说,除了用户登录,用户注册也是一个需要验证的流程。为了防止被恶意注册,有限资源被占用,一般都会需要能证明是真人的信息:手机号、身份证、邮箱、社交应用......但如果没有涉及到金融领域(支付、投资等),就不需要繁琐的认证,用户也可能不想把个人信息给你。
流程
如下图所示,这是一个简单的注册表,只有用户名、密码(没有重复确认密码)、邮箱、注册码这四个输入框。
点击“发送邮件注册码”按钮,就判断邮箱输入框中是否有输入,如果没有就提示,有就继续判断是否为正确的邮箱格式;如果不是正确的邮箱格式就提示,是就向把这个邮箱作为参数向后端接口发送请求;后端接收邮箱后,判断这个邮箱是否已经被注册了,如果已经被注册了,就返回提示给前端,没有就发送注册码到这个邮箱。
依赖
在项目中导入以下依赖,版本号与你的 Spring Boot 的版本一致就行,可以在 阿里云云效 Maven 中搜索。
<!-- mail 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- thymeleaf 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
与其它模板引擎相比,Thymeleaf 最大的特点是,即使不启动 Web 应用,也可以直接在浏览器中打开并正确显示模板页面。本文不讨论 Thymeleaf 的性能如何,以及为什么不用 Beetl。
配置
先要进入你的 QQ 邮箱,设置开启 SMTP 服务,官方说明:帮助中心。获取客户端的授权码,一般需要发送一条短信。
设置完毕之后,接着在项目的配置文件中加入以下相关配置。
# 字符集编码,默认 UTF-8
spring.mail.default-encoding=UTF-8
# SMTP 服务器 host,QQ 邮箱的为 smtp.qq.com;端口为 465 或 587
spring.mail.host=smtp.qq.com
# SMTP 服务器端口,不同的服务商不一样
spring.mail.port=465
# SMTP 服务器使用的协议
spring.mail.protocol=smtp
# 发送端的用户邮箱名
spring.mail.username=xxx@qq.com
# 发送端的密码,也就是获取的授权码
spring.mail.password=oooooxxxxxxxx
# 指定 mail 会话的 jndi 名称,一般我们不需要
spring.mail.jndi-name=xxx
# 指定是否在启动时测试邮件服务器连接,默认为 false
spring.mail.test-connection=false
Controller 层
Controller 层接收表单参数,可以使用 Map 接收 Post 请求参数,或者使用 HttpServeletRequest 的形式接收,还可以使用 Java bean 接收......我这里使用 String 类型接收单个参数:邮箱。发送注册码后,把邮箱和对应的注册码存储到 Redis 中,如果注册码尚未过期,用户继续请求时就不用再次发送了,还方便后续表单的校验,最后返回提示给前端。
@ApiOperation("获取邮件注册码")
@AnonymousPostMapping(value = "/emailCode")
public ResponseEntity<Object> getEmailCode(@RequestBody String param) {
Map<String, Object> sendResult = new HashMap<>(2);
JSONObject parse = JSONObject.parseObject(param);
// 获取 Email
String email = parse.getString("userEmail");
// 先查询该 Email 是否已经发送过注册码
if (redisUtils.hasKey(email)) {
// 如果获取到数据则直接返回
sendResult.put("regRes", 210);
sendResult.put("regTip", "已发送注册码,请检查邮箱。");
} else {
/**
* 再查询该 Email 是否被注册了
* 如果只是缓存过期了,在大量的请求中就只能有一个线程能够拿到查询数据库的锁,
* 而其他的请求只能进入等待状态,等待第一个线程更新缓存,其他线程再从更新的缓存中获得
*/
Integer exist = userService.findByEmail(email);
if (exist != null) {
// 邮箱已存在数据库中
sendResult.put("regRes", 210);
sendResult.put("regTip", "该邮箱已被注册使用,请更换邮箱。");
} else {
// 产生 6 位数的注册码,获得一个随机的字符串(只包含数字和字符)
String randomCode = RandomUtil.randomString(6);
// 发送
emailService.sendEmailCode(email, randomCode);
// 存储在 Redis 中,用于后续验证。key--邮箱账号;value--邮箱注册码
redisUtils.set(email, randomCode, 5, TimeUnit.MINUTES);
sendResult.put("regRes", 200);
sendResult.put("regTip", "注册码已发送,请留意您的邮箱。");
}
}
// 返回
return ResponseEntity.ok(sendResult);
}
Service 层
参数检验最好放在 Controller 层,而 Service 层聚焦于业务逻辑的处理,那就是处理发送邮件。实现类中的代码如下:
@Override
@Transactional(rollbackFor = Exception.class)
public void sendEmailCode(String email, String code) {
// 创建邮件正文
Context context = new Context();
// String 类型的注册码转 char 数组
char[] chars = code.toCharArray();
context.setVariable("verifyCode", chars);
// 将模块引擎内容解析成 HTML字符串
String emailContent = templateEngine.process("sendEmailCode", context);
MimeMessage message = mailSender.createMimeMessage();
// 发送
try {
// true 表示需要创建一个 multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(sendEmailUser, "一个靓仔");
helper.setTo(email);
helper.setSubject("注册验证码");
// 设置邮件内容,注意加参数 true,表示启用 Html 格式
helper.setText(emailContent, true);
mailSender.send(message);
} catch (MailException | MessagingException | UnsupportedEncodingException e) {
throw new BadRequestException(e.getMessage());
}
}
邮件模板
写 CSS 是很痛苦的,所以我直接从 CodePen 找到一个我比较喜欢的拟物风格的按钮:Neumorphism Button,然后从中复制了部分 CSS。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" content="emialCode">
<title>邮箱注册验证码</title>
<style>
body {
background:#eeeeee;
display:flex;
align-items:center;
justify-content:center;
}
table {
width: 700px;
margin: 0 auto;
}
#top {
width: 700px;
border-bottom: 1px solid #ccc;
margin: 0 auto 30px;
}
#top table {
font: 12px arial, 'Hiragino Sans GB', '\5b8b\4f53', sans-serif;
height: 40px;
}
#content {
width: 680px;
padding: 0 10px;
margin: 0 auto;
}
#content_top {
line-height: 1.5;
margin-bottom: 25px;
color: #4d4d4d;
}
#content_top strong {
display: block;
margin-bottom: 15px;
}
#content_top strong span {
font-size: 18px;
color: #FE4F70;
}
#verificationCode {
height: 100px;
width: 680px;
text-align: center;
margin: 30px 0;
color: #00b894;
font-size: 24px;
}
#content_bottom {
margin-bottom: 30px;
}
#content_bottom small {
display: block;
margin-bottom: 20px;
font-size: 12px;
color: #747474;
}
#bottom {
width: 700px;
margin: 0 auto;
}
#bottom div {
padding: 10px 10px 0;
border-top: 1px solid #ccc;
color: #747474;
margin-bottom: 20px;
line-height: 1.3em;
font-size: 12px;
}
#sign {
text-align: right;
font-size: 18px;
color: #FE4F70;
font-weight: bold;
}
.m_button {
-webkit-tap-highlight-color: rgba(0,0,0,0);
-webkit-tap-highlight-color: transparent;
display:flex;
float: left;
align-items:center;
justify-content:center;
flex-direction:column;
cursor:pointer;
background-color:#eeeeee;
width:80px;
height:80px;
margin-left: 30px;
border-radius:10px;
box-shadow: -7px -7px 20px 0px #fff9,
-4px -4px 5px 0px #fff9,
7px 7px 20px 0px #0002,
4px 4px 5px 0px #0001,
inset 0px 0px 0px 0px #fff9,
inset 0px 0px 0px 0px #0001,
inset 0px 0px 0px 0px #fff9,
inset 0px 0px 0px 0px #0001;
transition:box-shadow 0.6s cubic-bezier(.79,.21,.06,.81);
}
.m_button span{
font-size: 40px;
border-radius:4px;
margin:3px 0px 3px 0px;
transition:margin 0.4s cubic-bezier(.79,.21,.06,.81),transform 0.4s cubic-bezier(.79,.21,.06,.81);
}
.m_button:hover{
box-shadow: 0px 0px 0px 0px #fff9,
0px 0px 0px 0px #fff9,
0px 0px 0px 0px #0001,
0px 0px 0px 0px #0001,
inset -7px -7px 20px 0px #fff9,
inset -4px -4px 5px 0px #fff9,
inset 7px 7px 20px 0px #0003,
inset 4px 4px 5px 0px #0001;
}
</style>
</head>
<body>
<table>
<tbody>
<tr>
<td>
<div id="top">
<table>
<tbody><tr><td></td></tr></tbody>
</table>
</div>
<div id="content">
<div id="content_top">
<strong>尊敬的用户:您好!</strong>
<strong>
您正在进行<span>注册账号</span>操作,请在注册码输入框中输入以下注册码完成操作:
</strong>
<div id="verificationCode">
<div class="m_button" th:each="chara:${verifyCode}">
<span>[[${chara}]]</span>
</div>
</div>
</div>
<div id="content_bottom">
<small>
注意:此操作可能会修改您的密码、登录邮箱或绑定手机。如非本人操作,请勿泄露!
<br>(注册码有效期为 5 分钟,及时使用。)
</small>
</div>
</div>
<div id="bottom">
<div>
<p>此为系统邮件,无需回复<br>
请保管好您的邮箱,避免账号被他人盗用
</p>
<p id="sign">——NewADemo</p>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
DeBug
在运行过程中,遇到了一些错误。
异常 1:一开始我使用的是 Hutool 的 MailUtil 测试发送邮件,但总是会出现:MessagingException: Could not connect to SMTP host: smtp.qq.com, port: 465
,接着查询了一下这个异常信息,在 CSDN 博客中看到“JDK 1.8 的配置中禁止了这个 SLLv3 协议,所以本地发送邮件才会一直报错,需要删除它以及 TLSv1, TLSv1.1”,我就开始检查我的 JDK。
我的 JDK 版本是 11,而且不是 Oracle JDK,后来想起来是以前在开发 Mirai 插件时用的(推荐使用 AdoptOpenJDK),没切换回来。切换 JDK 后,就正常发送了。
异常 2:Error resolving template [sendEmailCode], template might not exist or might not be accessible by any of the configured Template Resolvers
,翻译一下就是模板不存在(没有引入依赖),或者模板无法被解析器解析(没有配置 Thymeleaf)。在 application.yml 中添加配置即可:
spring:
thymeleaf:
prefix: classpath:/template/email/
suffix: .html
mode: HTML
cache: false
encoding: UTF-8
异常 3:Got bad greeting from SMTP host: smtp.qq.com, port: 465, response: [EOF]. Failed messages:
,这个问题与配置有关:如果设置 spring.mail.port 为 465,需要将 spring.mail.protocol 设置为 smtps;如果端口改为 587,则可以使用 smtp。
异常 4:java.lang.IllegalArgumentException: To address must not be null
,这个就是收件人地址不能为空。注意一下参数是否传进来了。
前端
前端使用 Vue2,需要设置:点击了发送邮件按钮后,按钮就进入 90 秒的禁用状态,倒计时完毕后才可以再次点击。弹窗使用的是 sweetalert,它是一款漂亮的 JavaScript 弹窗,对于 Vue2 可以使用 vue-sweetalert2。
sendEmailCode() {
// 只对邮箱这一项单独进行验证
this.$refs['logonForm'].validateField(['useremail'], errorMsg => {
// 检查无误
if (!errorMsg) {
if (!this.canClick) {
return
}
this.canClick = false
this.sendEmailMsg = this.totalTime + 's后重新发送'
// 请求后端发送邮件
getEmailCode(this.logonForm.useremail).then(res => {
if (res.regRes === 200) {
this.$swal('成功', res.regTip, 'success')
} else if (res.regRes === 210) {
this.$swal('警告', res.regTip, 'warning')
this.canClick = false
}
})
// 倒计时
const clock = window.setInterval(() => {
this.totalTime--
this.sendEmailMsg = this.totalTime + 's后重新发送'
if (this.totalTime < 0) {
window.clearInterval(clock)
this.sendEmailMsg = '重新发送注册码'
this.totalTime = 90
// 这里重新开启计时
this.canClick = true
}
}, 1000)
} else {
this.$swal('错误', '您的注册邮箱好像不太对...', 'error')
return false
}
})
参考
CSDN - SpringBoot 发送邮箱验证码
CSDN - 本地代码无问题邮件却发送失败的问题