手把手教你用 Spring Boot 为「手迹」用户忘记密码功能


后端不背锅
原创
发布时间: 2026-03-31 14:55:39 | 阅读数 0收藏数 0评论数 0
封面
本文详细讲了重置密码功能的实现流程,包括账号验证、邮箱验证以及新密码重新设置。
1

vue路由配置

请在项目目录下的 router 文件夹中找到对应的路由配置文件,定位到其中的 routes 数组,并在该数组中添加以下路由项:

{
path: '/forgetPassword',
component: () => import('@/view/forgetPassword/index.vue')
}

这样即可完成密码找回页面的路由注册。

TS
routes.ts
1.12KB
2

验证邮箱页面

进入到忘记密码邮箱验证页面,在这个页面输入忘记密码的邮箱,邮箱必须为已注册的账号,未注册会有提示并且无法发送验证码。已注册的账号会进入到邮箱码验证环节。

<script setup lang="ts">

import {ref} from "vue";
import {sendEmailCode} from "@/api/auth/code.ts";
import {ElMessage} from "element-plus";
import {getUserExist} from "@/api/user/user.ts";
import CustomFromItem from "@/components/custom/CustomFromItem.vue";
import CustomInput from "@/components/custom/CustomInput.vue";
import CustomFrom from "@/components/custom/CustomFrom.vue";
import CustomFromBtn from "@/components/custom/CustomFromBtn.vue";

// 当前步骤
const active = defineModel('active');

// 邮箱
const email = defineModel('email', {default: ''})

const ForgetPasswordEmailForm = ref()


// 表单输入校验
const rules = {
email: [
{required: true, message: "请输入邮箱", trigger: "blur"},
{pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '邮箱格式不正确', trigger: "blur"},
{
validator: async (value: string) => {
return getUserExist(value).then(res => {
return res.data.code === 200 ? '邮箱不存在' : ""
})
}
}
],
}
// 发送验证码
const sendCode = () => {
// 判断表单校验是否通过
if (ForgetPasswordEmailForm.value?.isValid()) {
// 发送验证码
sendEmailCode(email.value).then(res => {
if (res.data.code === 200) {
active.value = 2
ElMessage.success("验证码已发送,请查看邮箱!")
} else {
ElMessage.error("发送失败,请稍后重试!")
}
})
}
}
</script>

<template>
<div>
<custom-from :validator="true" :rules="rules" ref="ForgetPasswordEmailForm">
<custom-from-item label="邮箱地址">
<custom-input placeholder="请输入邮箱" name="email" type="email" icon="src/assets/img/login/email.svg"
v-model:input-value="email"/>
</custom-from-item>
<custom-from-btn label="发送验证码" @click="sendCode"/>
</custom-from>
<p class="foot-text">想起来了?
<router-link class="foot-login" to="/login">返回登录 →</router-link>
</p>
</div>
</template>

<style scoped lang="scss">

// 文字描述
.foot-text {
color: rgb(107, 114, 128);
line-height: 24px;
font-size: 16px;
font-weight: 400;
padding-top: 24px;
}

// 跳转登录
.foot-login {
color: #4f39f6;
line-height: 24px;
font-weight: 500;
font-size: 14px;
}
</style>

注意事项:相关代码都放入了附件,整个项目完整的代码放在了末尾。

VUE
ForgetPasswordEmailStep.vue
2.33KB
3

发送验证码

在接收到发送邮件验证码的请求后,系统首先查询 Redis,检查该邮箱是否已在有效期内发送过验证码。若存在,则返回提示:“验证码已存在,请勿重复发送”;若不存在,则通过 CaptchaUtils 工具类生成一个六位随机数字验证码,并调用邮件服务发送该验证码至指定邮箱。邮件发送成功后,将该验证码以邮箱地址为 key、六位验证码为 value 存入 Redis,并设置合理的过期时间,用于后续校验。

/**
* 发送邮箱验证码
* 生成 6 位随机验证码并发送到指定邮箱
*
* @param email 接收验证码的邮箱地址
* @return true-发送成功,false-发送失败或验证码已存在
*/
@Override
public Boolean sendCode(String email) {
log.info("准备发送验证码到邮箱:{}", email);
// 检查是否已存在未过期的验证码
Boolean hasKey = redisUtils.hasKey(email);
if (hasKey) {
log.warn("验证码已存在,请勿重复发送:{}", email);
return false;
}
// 生成 6 位随机验证码
String code = CaptchaUtils.generateCaptcha();
// 发送邮件
Boolean sendMail = emailService.sendAuthCodeEmail("账号验证", email, code);
if (sendMail) {
// 将验证码存入 Redis,有效期 5 分钟
redisUtils.set(email, code, 5, TimeUnit.MINUTES);
log.info("验证码发送成功:{}", email);
return true;
}
log.error("验证码发送失败:{}", email);
return false;
}


4

邮箱发送邮件实现

邮箱发送功能会用到一些参数,这些参数都在application-dev.yml中配置。

package com.handwriting.config;

import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;

/**
* 邮件服务类
* 提供邮件发送功能,主要用于发送验证码邮件
* 配置项(需在 application.yml 中配置):
* - spring.mail.host: SMTP 服务器地址
* - spring.mail.port: SMTP 端口
* - spring.mail.username: 发件人邮箱
* - spring.mail.password: 邮箱授权码
*
*
* @author handwriting
* @version 1.0
* @date 2026/1/6
*/
@Component
@Slf4j
public class EmailService {

@Resource
private JavaMailSender mailSender;

/**
* 发件人邮箱地址
* 从配置文件中读取
*/
@Value("${spring.mail.username}")
private String from;

/**
* 发送验证码邮件
* 发送包含验证码的简单文本邮件
*
* @param subject 邮件主题
* @param email 收件人邮箱
* @param code 验证码内容
* @return true-发送成功,false-发送失败
*/
public Boolean sendAuthCodeEmail(String subject, String email, String code) {
log.info("准备发送邮件:收件人={}, 主题={}", email, subject);
try {
// 创建邮件消息
SimpleMailMessage mailMessage = new SimpleMailMessage();
// 发件人
mailMessage.setFrom(from);
// 收件人
mailMessage.setTo(email);
// 主题
mailMessage.setSubject(subject);
// 内容(验证码)
mailMessage.setText("您的验证码是:" + code + ",有效期 5 分钟,请勿泄露给他人。");
// 发送邮件
mailSender.send(mailMessage);
log.info("邮件发送成功:{}", email);
return true;
} catch (Exception e) {
log.error("邮件发送失败:{}, 错误:{}", email, e.getMessage());
return false;
}
}
}


5

验证邮箱码页面

账号验证成功后,系统将发送一封包含验证码的邮件。随后,您将进入“验证邮箱验证码”页面,在此输入收到的验证码进行校验:

  1. 若验证码正确,验证通过;
  2. 若输入错误,系统会提示相应错误信息;
  3. 您可随时点击“重新发送验证码”以获取新的验证码。
<script setup lang="ts">

import {ref} from "vue";
import {verifyCode} from "@/api/auth/code.ts";
import ErrorMessage from "@/components/custom/ErrorMessage.vue";
import CountDown from "@/components/custom/CountDown.vue";
import VerificationCode from "@/components/custom/VerificationCode.vue";
// 邮箱地址
const email = defineModel('email', {default: ''})

// 当前步骤
const active = defineModel('active');

// 验证码
const code = ref<string>('')

// 错误信息
const errorMsg = ref<string>('')
/**
* 完成邮箱验证
*/
const completeRegister = async () => {

// 是否输入验证码
if (code.value.length === 0) {
errorMsg.value = "请输入验证码"
return false
}

// 是否为6位数验证码
if (code.value.length < 6) {
errorMsg.value = "验证码为6位"
return false
}

// 校验验证码
const isVerifyCode = await verifyCode(email.value, code.value)

if (isVerifyCode.data.code === 500) {
errorMsg.value = "验证码输入错误,请重试"
return
}
active.value = 3
}
</script>

<template>
<div class="auth-from-wrapper">
<!-- 验证码输入框 -->
<VerificationCode v-model:code="code"/>
<error-message v-if="errorMsg" :error-content="errorMsg"/>
<!-- 验证码倒计时 -->
<count-down :email="email"/>
<!-- 验证-->
<button class="verification-submit" @click="completeRegister">验证</button>
</div>
</template>

<style scoped lang="scss">
.auth-from-wrapper {
display: flex;
gap: 15px;
flex-direction: column
}


// 验证码验证按钮
.verification-submit {
background-color: #E5E7EB;
color: #ffffff;
width: 100%;
padding: 12px 0;
line-height: 35px;
font-size: 18px;
border-radius: 12px;
display: flex;
justify-content: center;
font-weight: 600;
border: none;
outline: none;
transition-duration: 300ms;
background-image: linear-gradient(to right, #615fff, #9810fa, #ec4899);

// 移入抬起
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15),
0 2px 4px rgba(0, 0, 0, 0.1);
}

// 点击时 有按下的效果
&:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
}
</style>


VUE
ForgetPasswordCodeStep.vue
2.19KB
6

校验验证码

发送验证码后,进入验证码验证。实现如下:根据前端传入的邮箱地址和验证码,首先检查 Redis 中是否存在以该邮箱为 key 的记录,以确认是否已发送过验证码,并验证其是否仍在有效期内。若未发送或已过期,则视为验证失败。若验证有效,则从 Redis 中获取对应的验证码值,并与用户提交的验证码进行比对。若两者一致,则视为验证成功,返回“登录成功”,并立即删除 Redis 中该邮箱对应的键值对,防止重复使用。

/**
* 验证邮箱验证码
* 比对用户输入的验证码与 Redis 中存储的是否一致
*
* @param code 用户输入的验证码
* @param mail 邮箱地址
* @return true-验证成功,false-验证失败或验证码已过期
*/
@Override
public Boolean verifyCode(String code, String mail) {
log.info("验证邮箱验证码:{}", mail);
// 检查验证码是否存在(是否过期)
Boolean hasKey = redisUtils.hasKey(mail);
if (!hasKey) {
log.warn("验证码已过期或不存在:{}", mail);
return false;
}
// 获取 Redis 中存储的验证码
String mailCode = redisUtils.get(mail).toString();
// 比对验证码
if (code.equals(mailCode)) {
// 验证成功后删除验证码(防止重复使用)
redisUtils.delete(mail);
log.info("验证码验证成功:{}", mail);
return true;
} else {
log.warn("验证码错误:{}", mail);
return false;
}
}


7

重置密码页面

通过了邮箱的验证后,来到重置密码页面,在这个页面可以输入你新的账号密码。表单会校验两个密码是否一致,一致才会通过重置密码请求。

<script setup lang="ts">

import {ref} from "vue";
import CustomFromItem from "@/components/custom/CustomFromItem.vue";
import CustomInput from "@/components/custom/CustomInput.vue";
import CustomFrom from "@/components/custom/CustomFrom.vue";
import {findBackPassword} from "@/api/user/user.ts";
import type {ForgetPassword} from "@/types/interface/forgetPassoword/ForgetPasswordInfo.ts";
import {ElMessage} from "element-plus";

// 当前步骤
const active = defineModel('active');


// 邮箱地址
const forgetPasswordInfo = defineModel<ForgetPassword>('forgetPasswordInfo', {
default: {
email: '',
password: ''
}
})

// 确认密码
const confirmPassword = ref('')

const forgetPasswordResetInfoRef = ref()

// 表单输入校验
const rules = {
password: [
{required: true, message: "请输入至少六位数的密码", trigger: "blur"},
{min: 6, message: '请输入至少六位的密码', trigger: "blur"}
],
confirmPassword: [
{required: true, message: "请输入密码", trigger: "blur"},
{min: 6, message: "请输入至少六位的密码", trigger: "blur"},
{
validator: (value: any) => {
return value !== forgetPasswordInfo.value.password ? '两次输入的密码不一致' : ''
},
trigger: "blur"
}

]
}


/**
* 重置密码
*/
const resetPassword = () => {
if (forgetPasswordResetInfoRef.value?.isValid()) {
findBackPassword(forgetPasswordInfo.value).then((res) => {
if (res.data.code === 200) {
active.value = 4
ElMessage.success("密码重置成功!")
return
}
ElMessage.error("密码修改失败,请重试!")
})
}
}
</script>

<template>
<custom-from :validator="true" :rules="rules" ref="forgetPasswordResetInfoRef">
<custom-from-item label="密码">
<custom-input placeholder="至少输入6位" name="password" type="password"
icon="src/assets/img/login/password.svg" v-model:input-value="forgetPasswordInfo.password"/>
</custom-from-item>
<custom-from-item label="确认密码">
<custom-input placeholder="再次输入密码" name="confirmPassword" type="password"
icon="src/assets/img/login/password.svg"
v-model:input-value="confirmPassword"/>
</custom-from-item>

<button class="reset-password" @click="resetPassword"> 重置密码</button>
</custom-from>
</template>

<style scoped lang="scss">
// 表单点击按钮
.reset-password {
background-color: #E5E7EB;
color: #ffffff;
width: 100%;
padding: 12px 0;
line-height: 35px;
font-size: 18px;
border-radius: 12px;
display: flex;
justify-content: center;
font-weight: 600;
border: none;
outline: none;
transition-duration: 300ms;
background-image: linear-gradient(to right, #615fff, #9810fa, #ec4899);

// 移入抬起
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15),
0 2px 4px rgba(0, 0, 0, 0.1);
}

// 点击时 有按下的效果
&:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
}
</style>


8

UserDto

UserDto用于在用户忘记密码流程中接收前端提交的表单数据。


import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;

/**
* 用户注册 DTO
* 用于接收用户注册时提交的信息
* 包含校验注解,确保输入数据的有效性
*
* @author handwriting
* @version 1.0
* @date 2026/1/21
*/
@Data
public class UserDto {

/**
* 用户名
* 长度必须在 3-20 个字符之间,不能为空
*/
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在 3-20 个字符之间")
private String username;

/**
* 邮箱
* 必须符合邮箱格式,不能为空
*/
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;

/**
* 密码
* 长度必须在 6-20 个字符之间,不能为空
*/
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在 6-20 个字符之间")
private String password;

/**
* 邮箱验证码
* 注册时必须提供有效的邮箱验证码
*/
@NotBlank(message = "验证码不能为空")
private String code;
}


9

忘记密码Controller

该接口是“忘记密码”功能的入口,通过 @RequestBody 注解将前端传入的 JSON 请求体自动映射为 Java 对象。

/**
* 用户找回密码
* 通过邮箱来找回用户的密码
*
* @param userDto 找回信息
* @return 检查结果
*/
@PostMapping("/forget/password")
public R<Void> updateUserPassword(@RequestBody UserDto userDto) {
log.info("用户信息:{}", userDto);

boolean isUpdate = userService.findBackPassword(userDto);

// 修改成功
if (isUpdate) {
return R.success();
}

// 修改失败
return R.error(500, "密码修改失败,请重试");
}


10

忘记密码service

这是“忘记密码”功能的核心实现逻辑。方法通过用户提交的邮箱查找对应账户,若存在则使用安全的密码编码器(如 BCrypt)对新密码加密,并更新数据库中的用户记录。整个过程确保了数据一致性与密码安全性,仅当用户存在且更新成功时返回 true。

/**
* 找回密码
*
* @param userDto 用户信息
* @return 是否修改成功
*/
@Override
public boolean findBackPassword(UserDto userDto) {
// 创建查询条件
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
// 比对邮箱
queryWrapper.eq(User::getEmail, userDto.getEmail());
// 根据邮箱查找用户信息
User user = userMapper.selectOne(queryWrapper);
// 没有查到对应用户 返回失败
if (Objects.isNull(user)) {
return false;
}
// 加密密码进行存储
user.setPassword(passwordEncoder.encode(userDto.getPassword()));
// 通过id修改用户信息
int i = userMapper.updateById(user);

// 根据返回条数判断是否成功 i大于0 表示成功 反之失败
return i > 0;
}


11

最终页面

点击重置按钮修改成功后会进入到最终页面,然后返回登录输入账号密码即可登录。

ZIP
handwriting完整代码.zip
48.95MB
阅读记录0
点赞0
收藏0
禁止 本文未经作者允许授权,禁止转载
猜你喜欢
评论/提问(已发布 0 条)
评论 评论
收藏 收藏
分享 分享
pdf下载 下载
pdf下载 举报