引言
该篇文章是以SpringBoot+Vue3技术栈搭建的项目的邮箱注册登录流程,注册用户使用邮箱校验,使用qq邮箱发送验证码,并且把验证码存入Redis以备校验使用的详细介绍
干货部分
1.在SpringBoot项目的application.yaml文件中配置qq邮箱
spring:
# mail邮箱
mail:
# SMTP服务器(我用的是QQ邮箱的SMTP服务器地址,如果用的其它邮箱请另行百度搜索)
host: smtp.qq.com
# 发送验证码的邮箱(发件人的邮箱)
username: ********@qq.com
# 授权码 去qq邮箱的账户,开启服务,获取授权码
password: ********
# 编码
default-encoding: utf-8
# 其它参数
properties:
mail:
smtp:
# 如果是用SSL方式,需要配置如下属性,使用qq邮箱的话需要开启
ssl:
enable: true
required: true
# 邮件接收时间的限制,单位毫秒
timeout: 10000
# 连接时间的限制,单位毫秒
connection-timeout: 10000
# 邮件发送时间的限制,单位毫秒
write-timeout: 10000
2.使用代码,后端代码
服务层代码,首先验证邮箱是否为空,然后在调用发送验证码业务之前,我们先从Redis通过该key获取数据,看看是否已经存在,如果已经存在,则证明已经发送过验证码并且还未使用。那么我们可以根据业务逻辑去做相应的处理,我这里是告诉前台,已经发送过了。然后调用EmailUtil工具类的生成验证码的方法,然后创建邮箱对象,填写对应参数信息。
package com.example.bootdemo.services.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.bootdemo.domain.entity.UserInfo;
import com.example.bootdemo.helper.EmailUtil;
import com.example.bootdemo.mapper.UserInfoMapper;
import com.example.bootdemo.services.IUserInfoService;
import jakarta.annotation.Resource;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeUtility;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import org.apache.commons.lang3.StringUtils;
import java.util.concurrent.TimeUnit;
@Service
public class UserInfoService extends ServiceImpl<UserInfoMapper, UserInfo> implements IUserInfoService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private JavaMailSender javaMailSender;
@Resource
private EmailUtil emailUtil;
@Override
public String sendEmail(String email) {
if (StringUtils.isBlank(email)) {
throw new RuntimeException("未填写收件人邮箱");
}
// 定义Redis的key
String key = "msg_" + email;
//操作字符串
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
String verifyCode = valueOperations.get(key);
if (verifyCode == null) {
// 随机生成一个6位数字型的字符串
String code = emailUtil.generateVerifyCode(6);
// 邮件对象(邮件模板,根据自身业务修改)
SimpleMailMessage message = new SimpleMailMessage();
message.setSubject("**用户注册邮箱验证码");
message.setText("尊敬的用户您好!感谢您的注册。\n\n尊敬的: " + email + "您的校验验证码为: " + code + ",有效期5分钟,请不要把验证码信息泄露给其他人,如非本人请勿操作");
message.setTo(email);
try {
// 对方看到的发送人(发件人的邮箱,根据实际业务进行修改,一般填写的是企业邮箱)
message.setFrom(new InternetAddress(MimeUtility.encodeText("**ZWQ-INT") + "<**********@qq.com>").toString());
// 发送邮件
javaMailSender.send(message);
// 将生成的验证码存入Redis数据库中,并设置过期时间
valueOperations.set(key, code, 5L, TimeUnit.MINUTES);
log.warn ("邮件发送成功");
return "邮件发送成功";
} catch (Exception e) {
log.error("邮件发送出现异常");
log.error("异常信息为" + e.getMessage());
log.error("异常堆栈信息为-->");
return "邮件发送失败";
}
} else {
return "验证码已发送至您的邮箱,请注意查收";
}
}
}
工具类EmailUtil代码,封装了验证码校验,和生成随机验证码的功能。我们校验验证码的地方结合Redis的代码。校验成功,意味着我们已经使用了,就会去删除掉这个验证码的key。
package com.example.bootdemo.helper;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.Properties;
import java.util.Random;
/**
* @ClassName EmailUtil
* @Description 邮箱
* @Author zhangweiqi
* @Date 2024/9/12 下午4:09
*/
@Component
public class EmailUtil {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 随机生成指定长度字符串验证码
*
* @param length 验证码长度
*/
public String generateVerifyCode(int length) {
String strRange = "1234567890";
StringBuilder strBuilder = new StringBuilder();
for (int i = 0; i < length; ++i) {
char ch = strRange.charAt((new Random()).nextInt(strRange.length()));
strBuilder.append(ch);
}
return strBuilder.toString();
}
/**
* 校验验证码(可用作帐号登录、注册、修改信息等业务)
* 思路:先检查redis中是否有key位对应email的键值对,没有代表验证码过期;如果有就判断用户输入的验证码与value是否相同,进而判断验证码是否正确。
*/
public Integer checkVerifyCode(String email, String code) {
int result = 1;
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
String msgKey = "msg_" + email;
String verifyCode = valueOperations.get(msgKey);
/*校验验证码:0验证码错误、1验证码正确、2验证码过期*/
if (verifyCode == null) {
result = 2;
} else if (!code.equals(verifyCode)) {
result = 0;
}
// 如果验证码正确,则从redis删除
if (result == 1) {
stringRedisTemplate.delete(msgKey);
}
return result;
}
}
注册用户,要先校验验证码,然后通过后才去注册用户
//注册用户
@PostMapping("/registerUser")
public Result<Boolean> registerUser(@RequestBody RegisterDTO entity){
Result<Boolean> result = new Result<>();
//验证码校验
int res = emailUtil.checkVerifyCode(entity.getEmail(),entity.getCode());
if(res == 0){
result.setCode(StateCodeEnum.ERROR.getCode());
result.setMsg("验证码错误!");
result.setData(false);
return result;
}else if (res == 2){
result.setCode(StateCodeEnum.ERROR.getCode());
result.setMsg("验证码过期!");
result.setData(false);
return result;
};
//查询数据库是否已经存在
//获取用户数据
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", entity.getUsername()).eq("password", entity.getPassword());
UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);
if(userInfo!=null){
result.setCode(StateCodeEnum.ERROR.getCode());
result.setMsg("用户已存在,注册失败!");
result.setData(false);
return result;
}
try {
//插入用户表
userInfoMapper.insert(BeanUtil.copyProperties(entity, UserInfo.class));
result.setCode(StateCodeEnum.ERROR.getCode());
result.setMsg("注册成功,请登录!");
result.setData(true);
return result;
}catch (Exception e){
result.setCode(StateCodeEnum.ERROR.getCode());
result.setMsg(e.getMessage());
return result;
}
}
3.前端代码
登陆页面,login.vue
<template>
<el-form
ref="formRef"
:model="form"
label-width="auto"
:rules="rules"
style="margin: 0 auto"
>
<h1>ZWQ-INT</h1>
<el-form-item label="用户名:" prop="username">
<el-input
v-model="form.username"
placeholder="请输入用户名"
style="width: 240px"
:autocomplete="newPassword"
>
<template #prefix>
<el-icon><User /></el-icon
></template>
</el-input>
</el-form-item>
<el-form-item label="密码:" prop="password">
<el-input
v-model="form.password"
placeholder="请输入密码"
style="width: 240px"
show-password
autocomplete="off"
>
<template #prefix>
<el-icon><Lock /></el-icon></template
></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">登录</el-button>
<el-button type="primary" @click="registerFun">注册</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { reactive, ref } from "vue";
import axios from "axios";
import { useRouter } from "vue-router";
import { User, Lock } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
export default {
components: {
User,
Lock,
},
setup() {
const router = useRouter();
const formRef = ref();
const form = reactive({
username: "",
password: "",
//http://localhost:8082/zwq/getIsLogin
});
const rules = reactive({
username: [
{
required: true,
message: "用户名不能为空",
trigger: "blur",
},
],
password: [
{
required: true,
message: "密码不能为空",
trigger: "blur",
},
],
});
//后端接口地址
const backendUrl = "http://localhost:8082/zwq";
//点击登录
const submitForm = () => {
formRef.value
.validate()
.then(async () => {
if (form.username.trim() && form.password.trim()) {
try {
// 注意:GET请求通常通过查询参数传递数据,而不是请求体
// 所以我们将参数附加到URL上
// const response = await axios.get(
// `${backendUrl}/getIsLogin?username=${form.username}&password=${form.password}`
// );
// console.log(response);
//获取token
const response = await axios.post(`${backendUrl}/login`, {
username: form.username.trim(),
password: form.password.trim(),
});
// 假设后端返回了一个包含登录状态的对象,例如 { isLogin: true }
//获取token
if (response.data != null && response.data != "") {
if (response.data.code === 200) {
localStorage.setItem("access-admin", response.data.data);
ElMessage({ message: "登陆成功", type: "success" });
router.push("/index");
}
if (response.data.code === 500) {
ElMessage.error(response.data.msg);
}
}
//获取用户信息
} catch (error) {
ElMessage({ message: "登陆失败", type: "warning" });
}
}
})
.catch(() => {});
};
//点击注册
const registerFun = () => {
router.push("/register");
console.log("进入注册!");
};
//点击重置
const resetForm = () => {
form.password = "";
form.username = "";
};
return {
form,
rules,
submitForm,
resetForm,
registerFun,
formRef,
};
},
};
</script>
<style scoped>
.el-form {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column; /* 如果需要垂直居中 */
}
</style>
注册页面,register.vue
<template>
<el-form
ref="formRef"
:model="form"
label-width="auto"
style="margin: 0 auto"
:rules="rules"
>
<h1>注册页面</h1>
<el-form-item label="用户名:" prop="username">
<el-input
v-model="form.username"
placeholder="请输入用户名"
style="width: 350px"
autocomplete="off"
></el-input>
</el-form-item>
<el-form-item label="密码:" prop="password">
<el-input
v-model="form.password"
placeholder="请输入密码"
style="width: 350px"
show-password
autocomplete="off"
></el-input>
</el-form-item>
<el-form-item label="确认密码:" prop="againpassword">
<el-input
v-model="form.againpassword"
placeholder="请输入确认密码"
style="width: 350px"
show-password
autocomplete="off"
></el-input>
</el-form-item>
<el-form-item label="邮箱:" prop="email">
<el-input
v-model="form.email"
placeholder="请输入邮箱"
style="width: 243px; margin-right: 5px"
autocomplete="off"
></el-input>
<el-button
type="primary"
:disabled="isClick || countdown > 0"
@click="getCode"
>{{
countdown > 0 ? `${countdown}秒后重新获取` : "获取验证码"
}}</el-button
>
</el-form-item>
<el-form-item label="验证码:" prop="code">
<el-input
v-model="form.code"
placeholder="请输入验证码"
style="width: 350px"
autocomplete="off"
></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">提交</el-button>
<el-button @click="onCancel">取消</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { onMounted, reactive, ref } from "vue";
import { useRouter } from "vue-router";
import axios from "axios";
import { ElMessage } from "element-plus";
export default {
setup() {
const router = useRouter();
const formRef = ref();
//后端接口地址
const backendUrl = "http://localhost:8082/zwq";
// 编辑
const form = reactive({
username: "",
password: "",
againpassword: "",
email: "",
code: "",
});
onMounted(() => {});
const emailPattern = /[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
const isClick = ref(true);
//倒计时
const countdown = ref(0);
//校验
const rules = reactive({
username: [
{
required: true,
message: "用户名不能为空",
trigger: "blur",
},
],
password: [
{
required: true,
validator: (rule, value, callback) => {
if (!value) {
callback(new Error("密码不能为空"));
} else {
callback();
}
},
trigger: "change",
},
],
againpassword: [
{
required: true,
validator: (rule, value, callback) => {
if (value === "") {
callback(new Error("确认密码不能为空"));
} else {
if (value !== form.password) {
callback(new Error("两次密码输入不一致"));
} else {
callback();
}
}
},
trigger: "change",
},
],
email: [
{
required: true,
message: "邮箱不能为空",
trigger: "blur",
},
{
validator: (rule, value, callback) => {
if (!emailPattern.test(value)) {
callback(new Error("邮箱格式不正确"));
isClick.value = true;
} else {
isClick.value = false;
callback();
}
},
trigger: "blur",
},
],
code: [
{
required: true,
message: "验证码不能为空",
trigger: "blur",
},
],
});
const getCode = async () => {
try {
const response = await axios.post(`${backendUrl}/getCode`, {
email: form.email,
});
if (response.data != null && response.data != "") {
if (response.data.code !== 200) {
ElMessage.error(response.data.msg);
} else {
ElMessage({ message: response.data.msg, type: "success" });
countdown.value = 60;
const intervalId = setInterval(() => {
if (countdown.value > 0) {
countdown.value--;
} else {
clearInterval(intervalId);
}
}, 1000);
}
}
} catch (error) {
ElMessage.error("请求异常");
}
};
const onSubmit = () => {
console.log("提交前Form: ", form);
formRef.value
.validate()
.then(async () => {
try {
console.log(form);
const response = await axios.post(
`${backendUrl}/registerUser`,
form
);
if (response.data != null && response.data != "") {
if (!response.data.data) {
ElMessage({ message: response.data.msg, type: "warning" });
} else {
ElMessage({ message: response.data.msg, type: "success" });
router.push("/");
}
}
} catch (error) {
ElMessage.error("请求异常");
}
})
.catch(() => {});
};
const onCancel = async () => {
await router.push("/");
};
return {
rules,
form,
isClick,
countdown,
getCode,
onSubmit,
onCancel,
formRef,
};
},
};
</script>
<style>
.el-form {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column; /* 如果需要垂直居中 */
}
</style>
主页,index.vue
<template>
<div v-if="userData">
欢迎回来:{{ userData.username }}
<h1>这是首页</h1>
</div>
<div v-else>加载中...</div>
</template>
<!-- <template>
<div class="common-layout">
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-container>
<el-header>Header</el-header>
<el-main>Main</el-main>
</el-container>
</el-container>
</div>
</template> -->
<script>
import axios from "axios";
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
export default {
setup() {
const router = useRouter();
//后端接口地址
const backendUrl = "http://localhost:8082/zwq";
const userData = ref(null);
//钩子
onMounted(async () => {
try {
const token = localStorage.getItem("access-admin");
if (!token) {
// 处理没有 token 的情况
return;
}
const response = await axios({
method: "post",
url: `${backendUrl}/getUserData`,
headers: {
Authorization: `Bearer ${token}`,
},
});
userData.value = response.data; // 更新响应式引用的值
console.log(response);
if (response.data === "") {
alert("token验证失败,请重新登录!");
await router.push("/");
}
} catch (error) {
console.error("Error fetching user data:", error);
// 处理错误,比如显示错误消息或重定向到登录页面
}
});
return {
userData,
};
},
};
</script>
<style scoped></style>
效果
登陆页面
注册页面
邮箱验证码获取成功
我做了校验,重复用户信息,不准创建。
注册成功
登录成功
登陆成功,Redis的验证码清空
至此完成
总结
通过这篇文章,可以了解到,通过邮箱获取验证码,并把验证码存储到Redis里面,在注册时去Redis获取,和前端传过去的验证码做校验。来注册用户。此项目继承了JWT授权,如有想了解的,请前往Jwt授权,也是博主记录的。相对于之前的,前端接入了Element Plus UI,页面好看了不少。后端呢,封装了Result类去返回给前端。后续呢,前端会把调用接口封装一下,统一一下风格。后面这个初学项目代码会持续更新,慢慢完善。如有想要源码的,评论111。
有不对的地方欢迎指正,共同学习进步,祝您每天快乐✊。
标签:const,SpringBoot,验证码,result,Vue3,import,data,response From: https://blog.csdn.net/qq_53924122/article/details/142256574