前后端分离项目中实现注册业务中的邮箱验证码功能(详细)

一,前言

信息化的当前,信息安全性对于网站开发标准而言是不言而喻的。特别是用户进行登录注册时,防止恶意注册也成为网站开发的一项重点业务。由于现在有工具可以自动填入信息,在很短的时间内会注册大量的用户,这样会给网站服务器造成巨大压力,为了防止这种事情,所以发明了这种验证码,机器是不能识别的,只能是人才行。因此,今天带来一个在前后端分离的环境下,实现一个注册模块中,通过邮箱验证码实现用户注册的功能。起初是想通过短信验证码sms来实现的,但是现在好多短信业务要收费了,因此一切从简,以类似的方式,用邮箱验证码来代替实现效果。

验证码的作用:
  1、为了防止机器冒充人类做暴力破解
  2、防止大规模在线注册滥用服务
  3、防止滥用在线批量操
  4、防止自动发布
  5、防止信息被大量采集聚合

二,实现效果

先来看看基础的实现效果:

用户输入基础的注册信息后,需要填写自己的邮箱号,并在前端进行基本的邮箱格式校验,如果邮箱号没毛病,则通过邮箱号去向邮箱发送验证短信,并得到邮箱验证码信息。后端将用户得到并输入的验证码信息与Redis缓存验证码信息做对比,并校验用户注册信息与数据库是否吻合,才能得到成功的注册结果。

三,准备条件

3.1,邮箱发送服务准备

想要实现邮箱验证码发送功能务必要先开启与之对应的邮箱服务。

这里以网易邮箱为例,我们需要用自己的邮箱作为发送邮箱,就需要开启POP3/SMTP/IMAP。登录邮箱–设置–账户–开启POP3/SMTP/IMAP,开启时可能会有短信验证,开启后显示验证码之类的一串英文,复制保存起来,后面要用

qq邮箱在设置-》账户 下即可开启相关的服务。

二者都行,其他也可。如果你想用那种就开启对应服务,并在SpringBoot项目中的Yml文件中配置对应的服务信息即可。

因为笔者是前后端分离项目,因此在这还要准备一下基础的项目搭建过程。

3.2,后端环境搭建说明

SpringBoot项目搭建及用户数据库不做详情描述,这里只说一下主要需要的项目依赖。在SpringBoot项目中已经有了集成邮件发送功能的依赖包了,因此当我们想要使用邮箱发送功能时,创建基础项目模块后要导入其依赖。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

除此以外,还需要在项目的yml文件中配置邮箱服务地址,以及作为发送邮箱的对象信息。这里以配置QQ邮箱为例。基础配置如下:

spring:
  mail:
    # 发送者邮箱
    username: *********
    #申请到的授权码
    password: *********
    # 配置 SMTP 服务器地址
    host: smtp.qq.com
    # 端口号465或587
    port: 465
    protocol: smtps
    # 默认的邮件编码为UTF-8
    default-encoding: UTF-8
    # 配置SSL 加密工厂
    properties:
      mail:
        smtp:
          socketFactoryClass: javax.net.ssl.SSLSocketFactory
          #表示开启 DEBUG 模式,这样,邮件发送过程的日志会在控制台打印出来,方便排查错误
          debug: true
          ssl: true

这样后端邮箱服务配置准备就算完成了。

3.3,前端环境搭建说明

前端项目主要通过Vue框架搭建,配合ElementPlus即可构建出简单的注册界面,这里不做详述。主要业务逻辑在于:

当用户输入邮箱后,才能点击"获取邮箱验证码"按钮去获取邮箱验证码,并且第一次点击后按钮变为不可选,同时进入一分钟倒计时(与后端的邮箱验证码缓存时间一致)。只有当邮箱获取时间过后(此时后端的缓存消失),才能再次获取。这样就有效避免了恶意注册。

四,实现过程步骤

4.1,编写发送邮件业务

这里我们导入了SpringBoot集成邮箱功能的依赖后可以通过注入JavaMailSenderImpl对象来调用createMimeMessage()方法来生成MimeMessage类的对象,MimeMessage类是一个功能相当强大的邮件类,用来对邮件进行编码并发送。

package com.yy.util.mailUtil;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.time.Duration;

/**
 * @author young
 * @date 2022/12/7 18:41
 * @description: 发送邮箱业务
 */
@Component
public class MailMsg {

    @Resource
    private JavaMailSenderImpl mailSender;
    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    public boolean mail(String email) throws MessagingException {

        MimeMessage mimeMessage = mailSender.createMimeMessage();
        //生成随机验证码
        String code = CodeGeneratorUtil.generateCode(6);
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
        //设置一个html邮件信息
        helper.setText("<p style='color: blue'>三秦锅,你在搞什么飞机!你的验证码为:" + code + "(有效期为一分钟)</p>", true);
        //设置邮件主题名
        helper.setSubject("FlowerPotNet验证码----验证码");
        //发给谁-》邮箱地址
        helper.setTo(email);
        //谁发的-》发送人邮箱
        helper.setFrom("2463252763@qq.com");
        //将邮箱验证码以邮件地址为key存入redis,1分钟过期
        redisTemplate.opsForValue().set(email, code, Duration.ofMinutes(1));
        mailSender.send(mimeMessage);
        return true;
    }
}

这里需要注意的是,发送人setFrom虽然可填的是字符串,但是必须要和yml配置文件中发送者邮箱一致,我们也可以用@Value("${spring.mail.username}")将配置信息读取过来,但是不能瞎填,不然邮件发送就会失败。

这里笔者自定义了一个随机验证码生成工具。

/**
 * @author young
 * @date 2022/10/18 16:30
 * @description: 生成验证码
 */
public class CodeGeneratorUtil {
    /**
     * 生成指定长度的验证码
     * @param length 长度
     * @return
     */
    public static String generateCode(int length){
       return UUID.randomUUID().toString().substring(0, length);
    }

    /**
     * 雪花算法生成用户注册的id
     */
    public static long snowflake(){
        return IdUtil.getSnowflakeNextId();
    }
}

业务逻辑也很简单,当controller层调用该业务时,就通过该方法生成6位数的随机验证码,然后将验证码放进邮箱构建类工具中去生成邮件,并将验证码信息以输入的邮箱地址为key,验证码为value存入redis缓存。时间与前端“获取邮箱验证码一致”,为一分钟。

接下来构建controller即可。

4.2,构建后端业务接口

@Api(tags = "登录操作相关接口")
@RestController
@Slf4j
public class LoginController {
    @Autowired
    private MailMsg mailMsg;

    @ApiOperation(value = "发送邮箱验证码")
    @GetMapping(value = "sendEmail/{email}")
    @CostTime //aop自定义注解,监控接口执行时间
    public Result<Object> sendCode(@PathVariable String email) {
        log.info("邮箱码:{}",email);
        //从redis中取出验证码信息
        String code = redisTemplate.opsForValue().get(email);
        if (!StringUtils.isEmpty(code)) {
            return Result.ok().message(email + ":" + code + "已存在,还未过期");
        }
            boolean b = mailMsg.mail(email);
            if (b) {
                return Result.ok().message("验证码发送成功!");
            }
            return Result.fail().message("邮箱不正确或为空!");
    }
}

测试一下接口效果:

可以看到邮件发送成功了,但是呢!执行特别慢,通过AOP定义的执行时间监控来看,很不理想。

2023-03-17 22:07:00.105  INFO 2444 --- [nio-8084-exec-1] com.yy.aspect.LogAspect                  : 目前的访问时间为:2023年03月17日 22:07:00--访问接口为:Result com.yy.controller.LoginController.sendCode(String)
2023-03-17 22:07:00.124  INFO 2444 --- [nio-8084-exec-1] com.yy.controller.LoginController        : 邮箱码:263252763@qq.com
2023-03-17 22:07:00.507  INFO 2444 --- [nio-8084-exec-1] com.yy.aspect.ServiceAspect              : 调用的是服务层中的com.yy.controller.LoginController类,其中的的sendCode方法--执行时间为:20392毫秒

但是如果从前端界面发送的话,其实也是正常发送,不会太慢。只不过如果压测一下(模仿高并发)的话,延迟很明显了,这个我们结尾来优化……

4.3,构建前端用户注册界面

这个没什么好说的,以常规前后端分离方式构建,基本框架是Vue和ElemenPlus构建的前端界面。Axios封装后端请求。这里仅做页面参考,具体在于前端"获取邮箱验证码"的实现逻辑

<template>
  <div class="login-wrap">
    <div class="ms-register">
      <div class="title">账号注册</div>
      <el-form
          :model="registerForm"
          status-icon
          :rules="rules"
          label-width="70px"
      >
        <!--          账号-->
        <el-form-item prop="username" class="auto" label="账号">
          <el-input
              v-model="registerForm.username"
              placeholder="账号名"
              size="large"
          ></el-input>
        </el-form-item>
        <!--          密码-->
        <el-form-item prop="password" class="auto" label="密码">
          <el-input
              type="password"
              placeholder="密码"
              v-model="registerForm.password"
              size="large"
          ></el-input>
        </el-form-item>
        <!--          确认密码-->
        <el-form-item label="确认密码" class="auto" prop="checkPass">
          <el-input
              type="password"
              v-model="registerForm.checkPass"
              autocomplete="off"
              placeholder="再次输入密码"
              size="large"
          ></el-input>
        </el-form-item>
        <!--          邮箱-->
        <el-form-item prop="email" class="auto" label="邮箱">
          <el-input
              v-model="registerForm.email"
              placeholder="邮箱"
              size="large"
          ></el-input>
        </el-form-item>
        <!--        邮箱验证码-->
        <div style="display: flex">
          <el-form-item prop="code" class="auto" style="width: auto;" label="验证码">
            <el-input
                size="large"
                class="input-wid"
                v-model="registerForm.code"
                placeholder="邮箱验证码"
            ></el-input>
          </el-form-item>
          <div class="w-18px"></div>
          <el-button size="large" :disabled="isCounting" :loading="smsLoading" @click="handleSms">
            {{ label }}
          </el-button>
        </div>
        <div class="sign-up-btn">
          <el-button size="large" @click="goBack()">返回登录</el-button>
          <el-button size="large" type="primary" @click="handleSignUp">确定注册</el-button>
        </div>
      </el-form>
    </div>
  </div>

</template>

<script>
import useSmsCode from "@/util/CodeGenerator/utilForCode/useSmsCode"

let {label, isCounting, loading: smsLoading, getSmsCode} = useSmsCode();
import request from "@/util/postreq";
import {mixin} from "../../mixins/index"


export default {
  name: "RegisterPage",
  mixins: [mixin],
  data() {
    const validatePass2 = (rule, value, callback) => {
      if (value === "") {
        callback(new Error("请再次输入密码"));
      } else if (value !== this.registerForm.password) {
        callback(new Error("两次输入密码不一致!"));
      } else {
        callback();
      }
    };
    return {
      label: label,
      smsLoading: smsLoading,
      isCounting: isCounting,
      // 注册
      registerForm: {
        username: "",
        password: "",
        checkPass: "",
        email: "",
        code: "",
      },
      // 必填项校验规则
      rules: {
        username: [{required: true, trigger: "blur"}],
        password: [{required: true, trigger: "blur"}],
        checkPass: [{validator: validatePass2, trigger: "blur"}],
        email: [
          {message: "请输入邮箱地址", trigger: "blur"},
          {
            type: "email",
            message: "请输入正确的邮箱地址",
            trigger: ["blur", "change"],
          },
        ],
      },
      defaultUserPic: "../../assets/my.jpg",
    };
  },
  methods: {
    goBack() {
      this.$router.push("/login")
    },
    handleSms() {
      console.log("------------")
      getSmsCode(this.registerForm.email)
    },
    handleSignUp() {
      // TODO:这里需要在前端做必填项校验
      request.post('/customer/register', this.registerForm)
          .then((res) => {
            if (res.code === 200) {
              this.$notify({
                title: res.message,
                type: res.type,
              });
              setTimeout(() => {
                  this.goBack()
              }, 200);
            } else {
              this.$notify({
                title: res.message,
                type: "error",
              });
            }
          })
          .catch((err) => {
            console.error(err);
          });
    },
  },
};
</script>

<style scoped>
.title {
  margin-bottom: 50px;
  text-align: center;
  font-size: 30px;
  font-weight: 600;
  color: rgb(3, 19, 11);
}

.auto {
  display: flex;
  justify-content: center;
  align-items: center;
}

.w-18px {
  width: 18px;
}

.ms-register {
  position: absolute;
  left: 50%;
  top: 30%;
  width: 320px;
  height: 450px;
  margin: -150px 0 0 -190px;
  padding: 40px;
  border-radius: 5px;
  background: #fff;
}

.login-wrap {
  position: relative;
  background: url("../../assets/images/login.png") fixed center;
  background-size: cover;
  width: 100%;
  height: 100%;
}

.sign-up-btn {
  display: block;
  width: 100%;
  margin-top: 20px;
  text-align: center;
}
</style>

4.4,前端实现点击发送后的倒计时逻辑

首先就是倒计时设计,这里定义一个统计倒计时的js函数。

import { computed, onScopeDispose, ref } from 'vue';
import { useBoolean } from '../common';

/**
 * 倒计时
 * @param second - 倒计时的时间(s)
 */
export default function useCountDown(second) {
    if (second <= 0 && second % 1 !== 0) {
        throw new Error('倒计时的时间应该为一个正整数!');
    }
    const { bool: isComplete, setTrue, setFalse } = useBoolean(false);

    const counts = ref(0);
    const isCounting = computed(() => Boolean(counts.value));

    let intervalId;

    /**
     * 开始计时
     * @param updateSecond - 更改初时传入的倒计时时间
     */
    function start(updateSecond = second) {
        if (!counts.value) {
            setFalse();
            counts.value = updateSecond;
            intervalId = setInterval(() => {
                counts.value -= 1;
                if (counts.value <= 0) {
                    clearInterval(intervalId);
                    setTrue();
                }
            }, 1000);
        }
    }

    /**
     * 停止计时
     */
    function stop() {
        intervalId = clearInterval(intervalId);
        counts.value = 0;
    }

    onScopeDispose(stop);

    return {
        counts,
        isCounting,
        start,
        stop,
        isComplete
    };
}

定义组合函数useBoolean.js来记录状态改变。

import { ref } from 'vue';

/**
 * boolean组合式函数
 * @param initValue 初始值
 */
export default function useBoolean(initValue = false) {
    const bool = ref(initValue);

    function setBool(value) {
        bool.value = value;
    }
    function setTrue() {
        setBool(true);
    }
    function setFalse() {
        setBool(false);
    }
    function toggle() {
        setBool(!bool.value);
    }

    return {
        bool,
        setBool,
        setTrue,
        setFalse,
        toggle
    };
}

加载状态记录useLoading.js-->

import useBoolean from './useBoolean';

export default function useLoading(initValue = false) {
    const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initValue);

    return {
        loading,
        startLoading,
        endLoading
    };
}

邮箱规则校验封装函数regex.js-->


/** 手机号码正则 */
export const REGEXP_PHONE =
    /^[1](([3][0-9])|([4][01456789])|([5][012356789])|([6][2567])|([7][0-8])|([8][0-9])|([9][012356789]))[0-9]{8}$/;

/** 邮箱正则 */
export const REGEXP_EMAIL = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;

/** 密码正则(密码为6-18位数字/字符/符号的组合) */
export const REGEXP_PWD =
    /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){6,18}$/;

/** 6位数字验证码正则 */
export const REGEXP_CODE_SIX = /^\d{6}$/;

/** 4位数字验证码正则 */
export const REGEXP_CODE_FOUR = /^\d{4}$/;

/** url链接正则 */
export const REGEXP_URL =
    /(((^https?:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)$/;

这里主要用于邮箱格式校验,虽然elementui也可以校验,但是笔者这里还是加上了一层。可省略。

最后就可以定义发送邮箱验证码的函数了。当点击获取邮箱验证码后,前端界面按钮及时发生变化并开启倒计时,同时将前端向后端的请求与倒计时同步。

import { computed } from 'vue';
import { useLoading } from '../common';
import useCountDown from './useCountDown';
import {REGEXP_EMAIL} from '../config/regex';
import {HttpManager} from "@/api";
import {ElMessage} from "element-plus";

export default function useSmsCode(){
   const { loading, startLoading, endLoading } = useLoading();
   const { counts, start, isCounting } = useCountDown(60);

    const initLabel = '获取验证码';
    const countingLabel = (second) => `${second}秒后重新获取`;
    const label = computed(() => {
        let text = initLabel;
        if (loading.value) {
            text = '';
        }
        if (isCounting.value) {
            text = countingLabel(counts.value);
        }
        return text;
    });

    /** 判断邮箱格式是否正确 */
    function isEmailValid(email) {
        let valid = true;
        if (email === '') {
            ElMessage({
                message: '邮箱不能为空!',
                type:'error',
                grouping:true
            })
            valid = false;
        } else if (!REGEXP_EMAIL.test(email)) {
            ElMessage({
                message: '邮箱格式错误!',
                type:'error',
                grouping:true
            })
            valid = false;
        }
        return valid;
    }

    /**
     * 获取短信验证码
     * @param email
     */
     function getSmsCode(email) {
        console.log(email)
        const valid = isEmailValid(email);
        if (!valid || loading.value) return;
        startLoading();
        //向后端发起发送邮箱验证码请求
        const data =  HttpManager.sengEmail(email);
        if (data) {
            ElMessage({
                message: '验证码发送成功!',
                type:'success',
            })
            start();
        }
        endLoading();
    }

    return {
        label,
        start,
        isCounting,
        getSmsCode,
        loading
    }; 
}

4.5,后端完成用户注册controller接口

这里比较简单,就是一个前后端协同触发数据库添加操作了,详细的service就不做展示了。但是为了偷懒笔者把大量业务逻辑都放在了controller层,并不推荐这么做,业务逻辑最好还是放在业务类service上。

@RestController
@RequestMapping("/customer")
@Api(tags = "用户接口")
@Slf4j
public class CustomerController {

    private final CustomerServiceImpl customerService;
     /**
     * 不能用Resource,否则获取不到存得值
     */
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    public CustomerController(CustomerServiceImpl customerService) {
        this.customerService = customerService;
    }

/**
     * 用户注册
     *
     * @param
     * @return
     */
    @ApiOperation(value = "用户注册调用接口")
    @PostMapping(value = "/register")
    @CostTime
    public Result<Object> register(@RequestBody @Validated RegisterDto registerDto, BindingResult result) {
        if (result.hasErrors()) {
            return Result.fail().message("填写的信息不齐");
        }
        Customer customer = new Customer();
        Customer one = customerService.getOne(new QueryWrapper<Customer>().eq("username", registerDto.getUsername()));
        if (!StringUtils.isEmpty(one)) {
            log.info("没走验证业务?");
            return Result.fail().message("账号已存在,请重新注册");
        }
        //验证邮箱验证码
        String code = registerDto.getCode();
        log.info("前端输入的验证码{}", code);
        String eml = registerDto.getEmail();
        log.info("前端的对象为{},邮箱=》{}",registerDto,eml);
        String s = redisTemplate.opsForValue().get(eml);
        log.info("从redis中获取code->{}",s);
        if (Objects.equals(s, code)) {
            log.info("验证码正确{}", code);
            //通过雪花算法设置cid
            customer.setCid(CodeGeneratorUtil.snowflake());
            customer.setCreateTime(LocalDateTime.now());
            customer.setSex(true);
            customer.setAvatar(registerDto.getAvatar());
            customer.setUsername(registerDto.getUsername());
            customer.setEmail(registerDto.getEmail());
            //分配角色id
            customer.setRoleId(2);
            customer.setBirth(LocalDate.now());
            //对用户输入的密码用md5Hex加密
            customer.setPassword(DigestUtil.md5Hex(registerDto.getPassword()));
            boolean user = customerService.save(customer);
            if (user) {
                log.info("账号注册成功?");
                return Result.ok().message("账号注册成功");
            }
            log.info("账号注册失败?");
            return Result.fail().message("账号注册失败");
        } else {
            log.info("验证码错误?{}", s);
            return Result.fail().message("验证码错误,请重新输入");
        }
    }
}

这样整体的邮箱验证码发送,用户注册+邮箱验证码功能就完成了。

五,邮箱发送优化

前面提到了,直接在业务中调用邮箱发送实际上执行效率很慢,特别是结果反馈。因此,我们需要一个更高效的方法去处理。尽量让业务结果反馈更快,将请求与发送邮件两个操作异步化,从而来提高效率。这里笔者推荐一种解决方式---->使用消息中间件rabbitmq来处理。用户发起获取邮箱验证码请求后,后端直接将生成的验证码以及用户输入的邮箱地址丢到rabbitmq中去作为的生产消息,然后在生成邮件中消费消息中的code和email。

流程如下:

5.1,导入mq依赖

除了SpringBoot整合的邮件功能的依赖外还需要导入rabbitmq的相关整合依赖。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

5.2,配置rabbitmq的yml配置信息

spring:
  rabbitmq:
    host: rabbitmq的ip地址
    username: guest
    password: guest
    port: 5672
    #none 禁用发布确认模式;correlated 发布消息成功交到交换机后触发回调方法 ;simple单个发布确认模式
    publisher-confirm-type: correlated #配置发布确认模式
    #回退消息,交换机可以但队列出现问题后将消息返回
    publisher-returns: true
    connection-timeout: 15000
    #手动确认
    listener:
      simple:
        acknowledge-mode: manual
        prefetch: 100
    virtual-host: /

5.3,配置rabbitmq配置

这里主要是配置消息的jackson转换和rabbitmqTamplate中消息的回调,【mq的对列,交换机,以及对列与交换机绑定信息】,后面的对列交换机绑定这块也可以直接在监听方法中通过注解完成绑定。

package com.yy.myconfig;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author young
 * @date 2022/12/7 16:51
 * @description:
 */
@Configuration
@Slf4j
public class MQConfig {
    
    //发送邮件-对列名
    public static final String MAIL_QUEUE_NAME = "mail.queue";
    //交换机名
    public static final String MAIL_EXCHANGE_NAME = "mail.exchange";
    /**
     * routing-key
     **/
    public static final String MAIL_ROUTING_KEY = "mail.routingkey";

    /*注册bean*/
    @Bean("mailQueue")
    public Queue mailQueue() {
        return new Queue(MAIL_QUEUE_NAME);
    }

    @Bean("mailExchange")
    public DirectExchange directExchange() {
        return new DirectExchange(MAIL_EXCHANGE_NAME);
    }

    //绑定对列与交换机以及路由
    @Bean
    public Binding mailExchangeBinding(
            @Qualifier("mailQueue") Queue queue,
            @Qualifier("mailExchange") DirectExchange exchange
    ) {
        return BindingBuilder.bind(queue).to(exchange).with(MAIL_ROUTING_KEY);
    }

    //配置消息转换
    @Bean
    public MessageConverter jsonConverter() {
        return new Jackson2JsonMessageConverter();
    }

    //配置RabbitTemplate
    @Bean
    public RabbitTemplate rabbitTemplate(CachingConnectionFactory cachingConnectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);
        //消息是否成功发送到Exchange,配置消息回调
        rabbitTemplate.setConfirmCallback(((correlationData, b, s) -> {
            if (b) {
                log.info("消息发送成功");
            } else {
                log.info("消息发送失败,{},原因是:{}", correlationData, s);
            }
        }));
        //交换器无法根据自身类型和路由显找到一个符合条件的队列时的处理方式,// RabbitHQ会调用Basic.Return命令将消息返回给生产者
        rabbitTemplate.setMandatory(true);

        /**
         * 当消息不可到达目的地时将消息返回给生产者
         * @param message 消息体
         * @param i 错误代码编号
         * @param s 原因
         * @param s1 交换机名
         * @param s2 routingKey
         */
        rabbitTemplate.setReturnCallback(((message, i, s, s1, s2) -> {
            log.error("消息{},被交换机{}退回,退回原因:{},路由routingKey是:{}", new String(message.getBody()), s1, s, s2);
        }));
        return rabbitTemplate;
    }
}

5.4,创建消息对象重构发送邮箱验证码业务

首先创建一个关于邮箱信息的消息对象,其中包含邮箱地址和验证码信息。用与将对象信息转换成消息体让mq处理。

package com.yy.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @author young
 * @date 2022/12/7 18:44
 * @description: 邮件信息体
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MailInfo implements Serializable {
    private static final long serialVersionUID = 3976534613804947578L;
    //生成的验证码
    private String code;
    //收件方邮箱
    private String email;
}

重构邮箱发送业务,此时主要处理的就是将用户输入的邮箱地址以及生成的随机验证码扔到mq中去生成消息体。

package com.yy.util.mailUtil;


import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONUtil;
import com.yy.dto.MailInfo;
import com.yy.myconfig.MQConfig;
import com.yy.util.MyException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.stereotype.Component;

import org.springframework.util.StringUtils;

import java.nio.charset.StandardCharsets;
import java.time.Duration;

/**
 * @author young
 * @date 2022/10/18 14:26
 * @description:
 */
@Component
@Slf4j
public class SendEmailUtil {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private RabbitTemplate rabbitTemplate;
 
    public boolean sendEmail(String emailAddress) {
        if (!StringUtils.isEmpty(emailAddress)) {
            //生成随机验证码
            String code = CodeGeneratorUtil.generateCode(6);
            //设置发送邮件的唯一标识
            CorrelationData correlationData = new CorrelationData(IdUtil.simpleUUID());
            //将邮箱验证码以邮件地址为key存入redis,1分钟过期
            redisTemplate.opsForValue().set(emailAddress, code, Duration.ofMinutes(1));
            //构建消息对象
            MailInfo mailInfo = new MailInfo();
            mailInfo.setEmail(emailAddress);
            mailInfo.setCode(code);
            //将对象转为字符串
            String s = JSONUtil.toJsonStr(mailInfo);
            MessageProperties messageProperties = new MessageProperties();
            //处理消息转换警告--》WARN 3012 --- [ntContainer#0-1] o.s.a.s.c.Jackson2JsonMessageConverter   : Could not convert incoming message with content-type [text/plain], 'json' keyword missing.
            messageProperties.setContentType("application/json");
            Message message = new Message(s.getBytes(StandardCharsets.UTF_8), messageProperties);
            //将邮箱放入消息队列
            rabbitTemplate.convertAndSend(MQConfig.MAIL_EXCHANGE_NAME, MQConfig.MAIL_ROUTING_KEY, message, correlationData);
            return true;
        }
        return false;
    }
}

5.5,监听mq的消息信息并处理执行邮件发送

package com.yy.util.mailUtil;

import cn.hutool.json.JSONUtil;
import com.rabbitmq.client.Channel;
import com.yy.dto.MailInfo;
import com.yy.myconfig.MQConfig;
import com.yy.util.MyException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;


import javax.annotation.Resource;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.IOException;



/**
 * @author young
 * @date 2022/12/7 17:59
 * @description: mq监听处理消息,并执行发送邮件的操作
 */
@Component
@Slf4j
public class MQListener {
    @Resource
    private JavaMailSenderImpl mailSender;

    @RabbitListener(queues = MQConfig.MAIL_QUEUE_NAME)
    @RabbitHandler
    public void handler(Message message, Channel channel) throws IOException {
        String s = new String(message.getBody());
        log.info("打印该信息---》{}", s);
        //将之前存入的字符串s反序列化为对象
        MailInfo mailInfo = JSONUtil.toBean(s, MailInfo.class);
        //从消息队列获取邮箱和验证码
        String email = mailInfo.getEmail();
        String code = mailInfo.getCode();
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        try {
            //构建邮件信息
            MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
            helper.setText("<p style='color: blue'>您的验证码为:" + code + "(有效期为一分钟)</p>", true);
            helper.setSubject("FlowerPotNet验证码----验证码");
            helper.setTo(email);
            helper.setFrom("2463252763@qq.com");
            //调用邮箱服务发送邮件
            mailSender.send(mimeMessage);
            log.info("打印该信息-邮件发送成功--》{}", (Object) mimeMessage.getFrom());
            log.info("打印该信息---》{}", s);
            // 手动确认,只确认当条消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (MessagingException | IOException e) {
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            throw new MyException(501, e.getMessage());
        }
    }
}

此时执行消费消息操作,获取消息体中的邮箱地址,验证码信息,并通过这两个信息去构建邮件信息执行发送邮件,如果发送成功,手动确认该条消息,否则给出异常。

5.6,测试优化效果

2023-03-18 19:11:58.690  INFO 3012 --- [nio-8084-exec-8] com.yy.aspect.LogAspect                  : 目前的访问时间为:2023年03月18日 19:11:58--访问接口为:Result com.yy.controller.LoginController.sendCode(String)
2023-03-18 19:11:58.691  INFO 3012 --- [nio-8084-exec-8] com.yy.controller.LoginController        : 邮箱码:**********@qq.com
2023-03-18 19:11:58.769  INFO 3012 --- [nio-8084-exec-8] com.yy.aspect.ServiceAspect              : 调用的是服务层中的com.yy.controller.LoginController类,其中的的sendCode方法--执行时间为:78毫秒
2023-03-18 19:11:58.794  INFO 3012 --- [ntContainer#0-1] com.yy.util.mailUtil.MQListener          : 打印该信息---》{"code":"9481a9","email":"**********@qq.com"}
2023-03-18 19:11:58.800  INFO 3012 --- [nectionFactory3] com.yy.myconfig.MQConfig                 : 消息发送成功
2023-03-18 19:12:17.866  INFO 3012 --- [ntContainer#0-1] com.yy.util.mailUtil.MQListener          : 打印该信息-邮件发送成功--》[**********@qq.com]

可以看到,此时业务执行效率就提高很多了😅

这只是笔者按照自己的逻辑实现的,可能在rabbitmq的处理上还有很多待解决的问题,比如消息堆积之类的,有回退的消息无法处理等等。后续也会继续分享,不足之处欢迎指正~😄

  • 31
    点赞
  • 72
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
实现前后端分离邮箱注册需要以下步骤: 1. 在前端项目创建注册页面,包括邮箱输入框和提交按钮: ```html <template> <div> <label for="email">邮箱:</label> <input type="email" id="email" v-model="email"> <button @click="submit">提交</button> </div> </template> ``` 2. 在前端项目使用 axios 库发送请求到后端接口: ```javascript import axios from 'axios'; export default { data() { return { email: '' } }, methods: { submit() { axios.post('http://localhost:3000/api/register', { email: this.email }) .then(response => { // 处理注册成功逻辑 }) .catch(error => { // 处理注册失败逻辑 }); } } } ``` 3. 在后端项目创建注册接口,处理注册逻辑: ```javascript const express = require('express'); const bodyParser = require('body-parser'); const app = express(); app.use(bodyParser.json()); app.post('/api/register', (req, res) => { const email = req.body.email; // TODO: 将邮箱信息存储到数据库 res.json({ success: true }); }); app.listen(3000, () => { console.log('Server is running on http://localhost:3000'); }); ``` 4. 在后端项目使用数据库存储邮箱信息,可以使用 MongoDB 或者 MySQL 等关系型数据库: ```javascript const mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/my_database', { useNewUrlParser: true }); const userSchema = new mongoose.Schema({ email: { type: String, required: true } }); const User = mongoose.model('User', userSchema); app.post('/api/register', (req, res) => { const email = req.body.email; const user = new User({ email }); user.save(err => { if (err) { console.log(err); res.status(500).json({ success: false, message: '注册失败' }); } else { res.json({ success: true }); } }); }); ``` 以上是一个简单的前后端分离邮箱注册流程,需要根据具体业务逻辑进行调整和完善。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值