SpringBoot整合邮箱验证码实现用户注册

news2025/2/26 4:24:52

唠嗑部分

今天我们来分享一下在系统开发过程中,如何使用验证码来验证用户并完成用户注册

首先来看一下成品界面展示

image-20230605171952976

说一下以上注册功能的设计:

用户手动输入用户名(全数据库唯一)、密码、确认密码、邮箱地址(单个邮箱最多可注册3个用户)、正确的邮箱验证码,即可注册

先来展示一下

输入用户信息

image-20230605172346155

收到邮箱验证码

image-20230605172320988

注册成功

言归正传

使用验证码这种方式呢是比较常见的,我们在注册App的时候,会有手机验证码、邮箱的类似哈(发邮件是免费的),邮箱验证也能够保证其真实性,防止恶意用户非法注册

下面我们就来说一下,这一系列的实现思路

1、用户名唯一验证就省略了哈,不在此范围内

2、在用户填入信息时前端先做必传、格式验证。

3、邮箱验证通过后,点击发送验证码时,携带邮箱参数请求后端接口。

4、后端生成并发送验证码后,将验证码进行分布式存储,如存到redis,key为邮箱,value为验证码,失效时间设置3分钟。

5、用户在3分钟内收到验证码并填入注册表单,请求用户注册接口。

6、后端在收到注册请求后,首先验证参数的合法性,验证通过后,根据邮箱去redis查询验证码。

7、未查询到验证码,则说明验证码已经过期,返回验证码校验失败,查询到验证码后,与表单中的验证码进行比较,相同则继续注册逻辑,不同则返回验证码校验失败。

代码环节

1、Vue组件

<template>
  <div class="header-all">
      <!--...-->
    <el-dialog :visible.sync="loginFlag" width="500px" :close-on-click-modal="false" :show-close="true">
      <!--登录表单省略...-->
      <el-form ref="registerForm" v-show="!login" :model="registerInfo" :rules="registerRules" class="register-form"
               autocomplete="on"
               label-position="left">
        <div class="title-container">
          <h3 class="title" style="text-align: center;font-size: 20px;margin-bottom: 15px;">
            {{ sysInfo.sysTitle }}注册</h3>
        </div>

        <el-form-item prop="userName">
          <el-input
              prefix-icon="el-icon-user"
              ref="username"
              v-model="registerInfo.userName"
              placeholder="请输入用户名"
              name="userName"
              type="text"
              tabindex="1"
              autocomplete="on"
          />
        </el-form-item>

        <el-tooltip v-model="capsTooltip" content="Caps lock is On" placement="right" manual>
          <el-form-item prop="password">
            <el-input
                show-password
                prefix-icon="el-icon-lock"
                ref="password"
                v-model="registerInfo.password"
                placeholder="请输入账户密码"
                name="password"
                tabindex="2"
                autocomplete="on"
                @blur="capsTooltip = false"
            />
            <span class="show-pwd">
          </span>
          </el-form-item>
        </el-tooltip>

        <el-tooltip v-model="capsTooltip" content="Caps lock is On" placement="right" manual>
          <el-form-item prop="password2">
            <el-input
                show-password
                prefix-icon="el-icon-lock"
                ref="password2"
                v-model="registerInfo.password2"
                placeholder="请确认密码"
                name="password2"
                tabindex="2"
                autocomplete="on"
                @blur="capsTooltip = false"
            />
            <span class="show-pwd">
          </span>
          </el-form-item>
        </el-tooltip>

        <el-form-item prop="email">
          <el-input
              prefix-icon="el-icon-message"
              ref="email"
              v-model="registerInfo.email"
              placeholder="请输入邮箱地址,每个邮箱最多可绑定3个账号"
              name="email"
              type="text"
              tabindex="1"
              autocomplete="on"
          />
        </el-form-item>

        <el-form-item prop="code">
          <el-row :gutter="20">
            <el-col :span="12">
              <el-input
                  ref="code"
                  type="text"
                  prefix-icon="el-icon-key"
                  v-model="registerInfo.code"
                  placeholder="请输入邮箱验证码"
                  name="code"
                  tabindex="2"
                  autocomplete="on"
                  @keyup.enter.native="registerHandle"
              />
            </el-col>
            <el-col :span="6" :offset="3">
              <el-button type="primary" size="small" :disabled="pause" @click="sendEmailCode">获取验证码 <span v-if="pause">{{ time }}</span>
              </el-button>
            </el-col>
          </el-row>
        </el-form-item>

        <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;"
                   @click.native.prevent="registerHandle">注册
        </el-button>

        <div style="text-align: right">
          <el-link type="primary" :underline="false" @click.native="nativeToLogin">已有账号,去登录>></el-link>
        </div>
      </el-form>
    </el-dialog>

    <el-dialog
        title="新用户提示"
        :visible.sync="dialogVisible"
        width="30%">
      <span>系统检测到您是新用户,请及时更新信息</span>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="toUpdateSelfInfo">去更新</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
<!--引入...-->
const defaultRegisterInfo = {
  userName: '',
  password: '',
  password2: '',
  email: '',
  code: ''
}
export default {
  <!--...-->
  data() {
    var checkUserName = (rule, value, callback) => {
      const pattern = /^[A-Za-z0-9-_]+$/;
      if (!value) {
        return callback(new Error('登录名不能为空'));
      }
      if (pattern.test(value) && value.length <= 16) {
        checkUserNameExist({userName: value}).then(res => {
          if (res.data) {
            callback(new Error('登录名已存在'));
          } else {
            callback();
          }
        })
      } else {
        callback(new Error('登录名须由数字、英文字母、-、下划线(不包含、)组成,不大于16位'));
      }
    };
    var validatePass = (rule, value, callback) => {
      if (value === '') {
        callback(new Error('请输入密码'));
      } else {
        if (this.registerInfo.password !== '') {
          this.$refs.registerForm.validateField('password2');
        }
        callback();
      }
    };
    var validatePass2 = (rule, value, callback) => {
      if (value === '') {
        callback(new Error('请再次输入密码'));
      } else if (value !== this.registerInfo.password) {
        callback(new Error('两次输入密码不一致!'));
      } else {
        callback();
      }
    };
    var validateEmail = (rule, value, callback) => {
      const pattern = /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/;
      if (value && value !== '') {
        if (pattern.test(value)) {
          checkEmailBindStatus({email: value}).then(res => {
            if (res.data) {
              callback();
            } else {
              callback(new Error('当前邮箱绑定用户已达到上限'));
            }
          })
        } else {
          callback(new Error('邮箱格式不正确'));
        }
      } else {
        callback(new Error('邮箱为必传项'));
      }
    };
    return {
      defaultUserImg: defaultUserImg,
      refresh: true,
      baseUrl: URL_PREFIX,
      codeUrl: URL_PREFIX + '/auth/getCaptcha',
      rules: {
        username: [{required: true, trigger: 'blur', message: '用户名为必填项'}],
        password: [{required: true, trigger: 'blur', message: '密码为必填项'}],
        code: [{required: true, trigger: 'blur', message: '验证码为必填项'}]
      },
      loading: false,
      registerInfo: {
        userName: '',
        password: '',
        password2: '',
        email: '',
        code: ''
      },
      time: 30,
      pause: false,
      registerRules: {
        userName: [{validator: checkUserName, trigger: 'blur'}],
        password: [{validator: validatePass, trigger: 'blur'}],
        password2: [{validator: validatePass2, trigger: 'blur'}],
        email: [{validator: validateEmail, trigger: 'blur'}],
        code: [{required: true, trigger: 'blur', message: '验证码为必填项'}]
      },
      dialogVisible: false
    }
  },
  methods: {
    // 用户注册
    registerHandle() {
      this.$refs.registerForm.validate(valid => {
        if (valid) {
          this.loading = true
          const registerInfo = {
              ...this.registerInfo,
              password: this.$encruption(this.registerInfo.password), 
              password2: this.$encruption(this.registerInfo.password2)
          }
          register(registerInfo).then(res => {
            this.$message.success('用户注册成功')
            this.loading = false
            this.registerInfo = defaultRegisterInfo
            this.$refs.registerForm.resetFields()
            this.nativeToLogin()
          })
        } else {
          console.log('error submit!!')
          return false
        }
        setTimeout(() => {
          this.loading = false
        }, 2000)
      })
    },
    // 发送验证码
    sendEmailCode() {
      this.$refs.registerForm.validateField('email', valid => {
        if (!valid) {
          if (this.pause) {
            this.$message.error('操作太频繁,请稍后再试')
          } else {
            sendEmailCode({email: this.registerInfo.email}).then(res => {
              this.$message.success('验证码发送成功,请注意查收!')
              this.pause = true
              const timer = setInterval(() => {
                if (this.time > 0) {
                  this.time = this.time - 1;
                }
                if (this.time <= 0) {
                  this.pause = false;
                  clearInterval(timer)
                  this.time = 30
                }
              }, 1000)
            })
          }
        }
      });
    }
  }
}
</script>

<style scoped lang="less">
   <!--css省略-->
</style>

2、api

// 发送验证码
export function sendEmailCode(data = {}) {
  return request({
    url: URL_PREFIX + '/main/user/sendEmailCode',
    method: 'post',
    data
  })
}
// 注册
export function register(data = {}) {
  return request({
    url: URL_PREFIX + '/main/user/register',
    method: 'post',
    data
  })
}

3、服务端发送验证码

@PostMapping("/user/sendEmailCode")
@ApiOperation("发送验证码处理器")
public BaseResult sendEmailCode(@RequestBody @Validated SendEmailCodeDTO dto, HttpServletRequest request){
    BaseResult result = BaseResult.ok();
    baseService.sendEmailCode(dto, request, result);
    return result;
}

实现类

@Override
public void sendEmailCode(SendEmailCodeDTO dto, HttpServletRequest request, BaseResult result) {
    long startTime = System.currentTimeMillis();
    try {
        // 验证码发送频率控制验证
        String string = redisUtil.getString(redisUtil.getCacheKey(CachePrefixContent.PUBLIC_EMAIL_CODE_TIME, dto.getEmail()));
        if (StringUtils.hasLength(string)) {
            result.setCode(CurrencyErrorEnum.OPERA_ERROR.getCode());
            result.setMsg(CurrencyErrorEnum.OPERA_ERROR.getMsg() + ",验证码发送过于频繁,请稍后再试");
        } else {
            // 生成6位数验证码
            String code = StringUtil.generatorCode(6);
            // 将验证码存入redis,有效期3分钟
            redisUtil.set(redisUtil.getCacheKey(CachePrefixContent.PUBLIC_EMAIL_CODE, dto.getEmail()), code, 3L, TimeUnit.MINUTES);
            // 调用mail发送邮件
            sendMailUtil.sendMail(dto.getEmail(), "邮箱验证码", sendMailUtil.buildCodeContent(code));
            // 验证码发送频率控制
            redisUtil.set(redisUtil.getCacheKey(CachePrefixContent.PUBLIC_EMAIL_CODE_TIME, dto.getEmail()), dto.getEmail(), 30L, TimeUnit.SECONDS);
        }
    } catch (Exception e) {
        log.error("邮箱验证码发送失败,{}", e);
        result.setCode(CurrencyErrorEnum.OPERA_ERROR.getCode());
        result.setMsg(CurrencyErrorEnum.OPERA_ERROR.getMsg());
    } finally {
        long endTime = System.currentTimeMillis();
        log.info("【{}】【邮箱验证码发送接口】【{}ms】 \n入参:{}\n出参:{}", "发送验证码", endTime - startTime, dto, result);
    }
}

4、服务端用户注册接口

@PostMapping("/user/register")
@ApiOperation("新用户注册处理器")
public BaseResult register(@RequestBody @Validated UserRegirsterDTO dto, HttpServletRequest request){
    BaseResult result = BaseResult.ok();
    userService.register(dto, request, result);
    return result;
}

实现类,非必要代码就省略了哈

@Override
@Transactional
public void register(UserRegirsterDTO dto, HttpServletRequest request, BaseResult result) {
    long startTime = System.currentTimeMillis();
    try {
        // 因为传输过程中对密码进行加密了,先解密在验证长度
        String password = RSAUtil.decrypt(dto.getPassword(), commonConfig.getRsaPrivateKey());
        if (password.length() > 16) {
            result.setCode(CurrencyErrorEnum.REQUEST_PARAMETER_ERROR.getCode());
            result.setMsg(CurrencyErrorEnum.REQUEST_PARAMETER_ERROR.getMsg() + ",用户密码最大16个字符");
            return;
        }
        //  去redis获取邮箱验证码
        String sysCode = redisUtil.getString(redisUtil.getCacheKey(CachePrefixContent.PUBLIC_EMAIL_CODE, dto.getEmail()));
        // 验证码存在且与参数传递过来的相等
        if (StringUtils.hasLength(sysCode) && sysCode.equals(dto.getCode())) {
            // 清楚这个验证码
            redisUtil.removeKey(redisUtil.getCacheKey(CachePrefixContent.PUBLIC_EMAIL_CODE, dto.getEmail()));
            // 邮箱绑定账号数验证 ...
            // 用户名唯一验证 ...
            // 用户注册
            User user = new User();
            BeanUtils.copyProperties(dto, user);
            user.setCreateTime(LocalDateTime.now());
            user.setUserId(IdUtil.simpleUUID());
            user.setPassword(passwordEncoder.encode(password));
            int insert = userMapper.insert(user);
            // 用户注册后的一系列权限分配,初始化...
        } else {
            // 未查询到验证码,返回验证码校验失败
            result.setCode(CurrencyErrorEnum.OPERA_ERROR.getCode());
            result.setMsg(CurrencyErrorEnum.OPERA_ERROR.getMsg() + ",验证码校验失败");
        }
    } catch (Exception e) {
        log.error("新用户注册失败,{}", e);
        result.setCode(CurrencyErrorEnum.OPERA_ERROR.getCode());
        result.setMsg(CurrencyErrorEnum.OPERA_ERROR.getMsg());
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    } finally {
        long endTime = System.currentTimeMillis();
        log.info("【{}】【新用户注册接口】【{}ms】 \n入参:{}\n出参:{}", "新增", endTime - startTime, JSON.toJSONString(dto), result);
    }
}

结语

今天这个案例就分享到这,总结一下

1、在发送请求时,作为前端需根据需求严格的对参数进行较验,无误后发送请求。

2、作为后端来讲,接口设计应极为严格,需自行参数验证,不能相信前端,因为很有可能,设计的接口会脱离浏览器被访问。

3、此案例代码较多,部分不相关的代码省略了,请周知。

4、制作不易,一键三连再走吧,您的支持永远是我最大的动力!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/618861.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Arm 推出 2023 全面计算解决方案,加速终端 AI 应用开发和部署

在当今数字化时代&#xff0c;人们对移动端计算能力的要求已经上升到了前所未有的高度。他们需要移动设备具有更快、更先进、更持久的计算能力&#xff0c;以提高生产力和生活质量。而科技厂商在满足人们对移动端计算能力的需求的同时&#xff0c;还需要从整个生态系统的角度出…

通过python封装接口seller_nick获取京东店铺所有商品数据,京东店铺所有商品数据接口,京东API接口

目的&#xff1a; 通过python封装接口seller_nick获取京东店铺所有商品数据&#xff0c;方法如下&#xff1a; 使用京东开放平台提供的API接口文档&#xff0c;找到seller_nick接口的具体参数及请求方式。 使用Python中的requests库发送请求&#xff0c;获取接口返回的数据。 …

nuxt3.0学习-三、nuxt.config.ts配置、跨域处理以及浏览器适配处理

nuxt官方对于nuxt.config.ts配置的介绍在Nuxt3.0 nuxt.config.ts配置 关于如何配置本人只能给出一点点启发&#xff0c;具体的配置需要根据个人需求去配置 nuxt.config.ts配置、跨域处理 import { prismjsPlugin } from "vite-plugin-prismjs"; export default de…

CompletableFuture异步和线程池

一、线程回顾 1、初始化线程的 4 种方式 1&#xff09;、继承 Thread 2&#xff09;、实现 Runnable 接口 3&#xff09;、实现 Callable 接口 FutureTask &#xff08;可以拿到返回结果&#xff0c;可以处理异常&#xff09; 4&#xff09;、线程池 方式 1 和方式 2&am…

《精通特征工程》学习笔记(1):数值特征处理

不进行多余的解释&#xff0c;想看原文直接下载pdf查看&#xff0c;本文是精简提炼了重要的方法写进来。 1.二值化 在百万歌曲数据集中&#xff0c;原始的收听次数并不是衡量用户喜好的强壮指标。&#xff08;在统计学术语 中&#xff0c;“强壮”意味着该方法适用于各种情况…

【2611. 老鼠和奶酪】

来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 有两只老鼠和 n 块不同类型的奶酪&#xff0c;每块奶酪都只能被其中一只老鼠吃掉。 下标为 i 处的奶酪被吃掉的得分为&#xff1a; 如果第一只老鼠吃掉&#xff0c;则得分为 reward1[i] 。如果第二…

Hive之HPLSQL安装手册

软件版本信息&#xff1a; CDH&#xff1a; cdh5.14.0 Hive: 1.1.0 Impala&#xff1a;2.11.0一&#xff1a;下载地址 Hplsql官网&#xff1a; http:www.hplsql.org/download 下载的是&#xff1a;hplsql-0.3.31.tar.gz版本 二&#xff1a;安装步骤 解压下载的hplsql-0.3.…

kafka 02

4.API开发 准备&#xff1a; 创建项目 &#xff0c; 添加依赖 XML <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <depen…

如何评价编程语言 Reason?

Reason编程语言的设计目标是为开发人员提供一种易于理解和学习的工具&#xff0c;同时提供静态类型检查和高效的代码执行。Reason基于OCaml语言&#xff0c;并引入了JavaScript的语法和工具链&#xff0c;使得开发者能够在现有的JavaScript生态系统中更好地开发和维护代码。Rea…

2023,数据库国产替代走到哪了?

如今&#xff0c;战场不仅银行&#xff0c;参战者也不仅单独的一家。对中国的国产数据库而言&#xff0c;机会和挑战都在加速涌来。 作者|思杭 编辑|皮爷 出品|产业家 2023&#xff0c;数据库格局正在变化&#xff0c;愈演愈烈。 如果说哪个环节是如今国产替代的最火热…

chatgpt赋能python:Python中如何输出两个数

Python中如何输出两个数 对于任何一种编程语言来说&#xff0c;输出两个数都是非常基础的知识点&#xff0c;Python也不例外。在Python中&#xff0c;我们可以使用print()函数来输出两个数。本篇文章将会介绍如何在Python中输出两个数。 介绍 在Python中&#xff0c;输出两个…

Python让文档处理变得轻松:如何快速替换Word文档中的关键字

应用场景&#xff1a; Python自动化处理Word文档的功能可以应用于许多场景&#xff0c;以下是其中一些常见的应用场景&#xff1a; 批量处理文档&#xff1a;如果您需要处理大量的Word文档&#xff0c;例如替换文本、添加文本、修改格式等&#xff0c;手动完成这些任务将非常耗…

驱动LSM6DS3TR-C实现高效运动检测与数据采集(4)----上报匿名上位机实现可视化

概述 LSM6DS3TR-C是单芯片“3轴陀螺仪 3轴加速度计”的惯性 测量单元(IMU)&#xff0c; 五种种可选满量程的陀螺仪(125/250/500/1000/2000 dps)和加速度计(2/4/8/16 g)。 上述工程中选择的加速度和陀螺仪对应的量程为2g和2000dps&#xff0c;对应的灵敏度如下所示&#xff0c…

具有更多存储空间和带宽的亚马逊云科技Snowball Edge存储优化型设备

亚马逊云科技AWS Snow Family系列设备用于将数据经济高效地迁移到云端并在边缘进行处理。增强型Snowball Edge存储优化型设备专为PB级数据迁移项目而设计&#xff0c;具有210TB的NVMe存储空间&#xff0c;每秒可传输高达1.5千兆字节的数据。这些设备还包括10GBASE-T、SFP48和QS…

缩略图加密学习总结

一、缩略图加密概述 完全加密为噪声图像后&#xff0c;密文图像的文件扩展&#xff0c;传输存储消耗更多的资源。完全加密的噪声图像的可用性建立在对密文进行解密的基础上&#xff0c;耗费大量的计算代价。原始图像中精细的视觉信息被抹去以保护隐私,而粗略的视觉信息被保留以…

Vue+element UI实现列表全部数据排序

element ui 表格中的sortable属性只能实现当前页数据的排序&#xff0c;无法实现整张表全部数据的排序&#xff0c;所以需要采取自定义的排序方式重新触发接口&#xff0c;获取排序好的全部列表 Java后端的分页列表有这两个字段需要前端去传递&#xff1a; el-table 上加上so…

springboot 2.6.12 自定义解析 yaml 加密数据

文章目录 一、简介二、yaml 默认解析简单说明三、自定义 yaml 解密解析器四、配置 PropertySourceLoader五、简单测试 一、简介 为了保证项目的配置文件的安全性&#xff0c;需要对第三方组件 mysql、redis、es、mq 等用户名密码进行加密处理&#xff0c;可以使用现成的三方包…

【springboot项目开发】文件上传与下载

目录 总体介绍 文件上传 介绍 文件上传的前端需求 文件上传的前端代码 文件上传的后端需求 文件上传的后端代码 文件下载 介绍 前端需求 前端代码 后端需求 后端代码 总体介绍 文件的上传和下载功能&#xff0c;是项目开发过程中比较常见的业务需求&#xff0c;我们…

开源 Golang 微服务入门二:RPC 框架 Kitex

前言 前一篇笔记介绍了字节跳动的开源 Golang 微服务 HTTP 框架 Hertz&#xff0c; 如下&#xff1a; 开源 Golang 微服务入门一&#xff1a; HTTP 框架 Hertz 本文将要介绍同样是字节跳动开源的 Golang 微服务 RPC 框架 Kitex。 Kitex 简介 Kitex 字节跳动内部的 Golang 微…

nacos高级

一、什么是配置中心 在微服务架构中&#xff0c;当系统从一个单体应用被拆分成分布式系统上的一个个服务节点后&#xff0c;配置文件也必须跟着迁移&#xff08;分割&#xff09;&#xff0c;这样配置就分散了。不仅配置会分散&#xff0c;分散中还会包含着冗余。 配置中心将…