SpringBoot+Vue3项目邮箱验证码注册详细教程

1 篇文章 0 订阅
1 篇文章 0 订阅

引言

该篇文章是以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。

有不对的地方欢迎指正,共同学习进步,祝您每天快乐✊。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值