Vue + element + Springboot 通过邮箱找回密码

Vue + element +Springboot 通过邮箱找回密码

  • 需求分析
    • 一、导入
    • 二、流程分析
  • 详细设计
    • 一、前端界面设计
      • 1. 登录界面
      • 2. 重置密码界面
    • 二、后端代码设计
      • 1. JavaMail配置
      • 2. QQ邮箱开启STMP授权
      • 3. 配置applicaiton.yml文件
      • 4. 新建文件夹
      • 5. 邮件配置:
      • 6. User相关类:User.java、UserMapper、UserService.java、UserServiceImpl.java
      • 7. ResetPassword相关类
      • 8. 数据库创建表格
      • 9. 编写控制器LoginController
      • 10. Result类
  • 效果展示
    • 一、登录界面
    • 二、找回密码
    • 三、输入邮箱
    • 四、输入验证码
    • 五、修改密码
  • 结尾
  • 查漏补缺

需求分析

一、导入

        接上一篇登录界面 拼图验证, 项目开发过程中还有一个需求,实现 通过邮箱重置密码

        重置密码,作为一个正经网站,那都是必备的需求,那咱可不得整一个!

        但是鉴于设计的范围有点广,先列出我觉得你需要会一点的东西,否则,直接dang代码可能报很多错误。

前端知识:

  • Vue.js
  • axios
  • element

后端知识:

  • springboot

二、流程分析

Vue + element + Springboot 通过邮箱找回密码_第1张图片
文字解释:

1. 根据用户注册时输入的邮箱账号,当进入重置密码界面时,先输入用户名和邮箱,前端表单将输入送到后端时,判断数据库中是否有对应的账户及邮箱
2. 如果存在,则后端发送一份携带验证码html邮件到用户邮箱,用户复制验证码到前端页面
3. 提交验证码到后端,后端根据用户名和验证码验证是否匹配,若匹配成功,则进入到输入密码的板块;若过期,则从 1开始重置密码。
4. 最后将当前的用户名密码重置为新密码。
5. 一个账号一天之内只能重置3次,超出后当天不在让其重置密码。

详细设计

一、前端界面设计

前端项目配置,首先需要安装axios, 用来跨域传输数据,以及element,因为绘制组件的时候用到了,这里我就默认你们都有了。

1. 登录界面

为什么要登录界面呢?

  1. 一般用户都是输入密码多次出错后,才会开始重置密码,所以在登录界面提供重置密码链接。
  2. 登录界面需要传递用户输入的用户名/邮箱,咱不能让张三重置李四的账号密码(好像也不会,就算知道了李四的邮箱,但是你也得登录李四的邮箱之后才能获得邮件邮件码)。

Login.vue

<template>
    <div id="login">
        <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
            <el-form :model="loginForm" :rules="rules" class="login-container" size="medium" @keyup.enter.native="handleClick">
                <h3 class="login_title">用户登录</h3>
                <el-form-item prop="username" >
                    <el-input type="text" v-model="loginForm.username" autofocus ref="username"
                              auto-complete="off" placeholder="用户名/邮箱" prefix-icon="el-icon-user-solid" spellcheck="false">
                    </el-input>
                </el-form-item>
                <el-form-item prop="password">
                    <el-input type="password" v-model="loginForm.password" autofocus ref="password"
                              auto-complete="off" placeholder="密码" prefix-icon="el-icon-key" v-on:input="change">
                    </el-input>
                </el-form-item>
                <el-form-item style="width: 100%;">
                    <el-checkbox class="login_remember" v-model="checked" >
                        <span style="color: #409EFF">记住我</span>
                        <label style="color: #949493">不是自己电脑请勿勾选</label>
                    </el-checkbox>
                    <el-button type="text" style="float: right; text-decoration: none; color: #50b6ff" @click="beforeTo">找回密码?</el-button>
                </el-form-item>
                <el-form-item style="width: 100%">
                    <el-button type="primary" class="button_login" >登录</el-button>
                    <router-link to="register">
                        <el-button type="primary" class="button_register">注册</el-button>
                    </router-link>
                </el-form-item>
            </el-form>
        </el-col>
        <p class="login-copyright">© 2020 lkq 版权所有</p>
    </div>
</template>

<script>
export default {
    name: 'Login',
    data() {
        return {
            isVerificationShow: false,
            rules: {
                username: [{required: true, message: '用户名或邮箱不能为空', trigger: 'change'}],
                password: [{required: true, message: '密码不能为空', trigger: 'change'}]
            },
            checked: true,
            loginForm: {
                username: '',
                password: ''
            },
            puzzleImgList: [
                require("../../assets/images/verify/1.jpg"),
                require("../../assets/images/verify/2.jpg"),
                require("../../assets/images/verify/3.jpg"),
                require("../../assets/images/verify/4.jpg"),
                require("../../assets/images/verify/5.jpg"),
                require("../../assets/images/verify/6.jpg"),
                require("../../assets/images/verify/7.jpg"),
                require("../../assets/images/verify/8.jpg"),
                require("../../assets/images/verify/9.jpg"),
                require("../../assets/images/verify/10.jpg"),
            ],
            isInput: false,
        }
    },
    methods: {
        handleSuccess() {
            // 验证通过后关闭图片验证
            this.isVerificationShow = false;
            // 将数据传送到后端验证
            this.login()
        },
        handleError() {
            // 滑动验证失败
            console.log("验证失败")
        },
        handleClick() {
            if (this.loginForm.username === '' || this.loginForm.password === '') {
                // 点击登录时,如果用户名或者密码未输入,那么提醒用户输入
                if (this.loginForm.username === '') {
                    this.$message({
                        message: '警告, 用户名或邮箱未输入哦',
                        type: 'warning'
                    });
                    this.$refs.username.focus();
                }else {
                    this.$message({
                        message: '警告, 密码未输入呀',
                        type: 'warning'
                    });
                    this.$refs.password.focus();
                }
            }else {
                this.isVerificationShow = true;
            }

        },
        change() {
           // 如果监听到输入框发生变化,那么采用用户输入的密码
           this.isInput = true;
        },
        beforeTo() {
            if (this.loginForm.username === '') {
                this.$notify({
                    title: '跳转失败',
                    message: "输入用户名或邮箱再去重置密码吧",
                    type: 'error'
                });
            }else {
                this.$router.push({
                    name: 'ResetPassword',
                    query: {
                        username: this.loginForm.username,
                    }
                })
            }
        }
    }
}
</script>

<style scoped >
    #login {
    background-image: url('../login_img.jpg');
    background-repeat: no-repeat;
    background-size: cover;
    height: 100%;
    width: 100%;
    position: fixed;
}
.login-container {
    border-radius: 15px;
    background-clip: padding-box;
    margin: 10% 40% 0 40%;
    width: 20%;
    padding: 25px 30px;
    background: #fff;
    border: 1px solid #eaeaea;
    box-shadow: 0 0 25px #cac6c6;
    opacity: 0.7;
}
.login_title {
    margin: 0px auto 40px auto;
    text-align: center;
    color: #505458;
}
.login_remember {
    margin: 0px;
    text-align: left;
    float: left;

}
.button_login {
    width: 40%;
    background: #409EFF;
    border: none;
    float: left
}

.button_register {
    width: 40%;
    background: #505458;
    border: none;
    float: right;
}
.login-copyright {
    color: #eee;
    padding-bottom: 20px;
    text-align: center;
    position: relative;
    z-index: 1;
}
@media screen and (min-height: 550px) {
    .login-copyright {
        position: absolute;
        bottom: 0;
        right: 0;
        left: 0;
    }
}
</style>

稍稍解释一下下:

  • :model="loginForm" : 表单的数据对象。
  • :rules="rules : 表单验证规则。
  • @keyup.enter.natice="handleClick": enter键函数,用户输入数据后可以直接enter键,不一定要点击按钮。
  • 记住我:利用cookie来存储登录信息,不是重点。
  • 找回密码:利用邮箱找回密码,重点。

界面效果展示:

2. 重置密码界面

ResetPassword.vue

<template>
    <div class="resetPassword">
        <div class="container">
            <el-steps :active="active" :space="200" finish-status="success"  align-center>
                <el-step title="验证用户名和邮箱" icon="el-icon-edit"></el-step>
                <el-step title="输入验证码" icon="el-icon-s-promotion"></el-step>
                <el-step title="设置新密码" icon="el-icon-key"></el-step>
            </el-steps>
            <div v-if="active === 0" class="common_div">
                <el-form :model="Form"  class="user-container" label-position="left" label-width="60px" size="medium">
                    <el-form-item  style="float: right; width: 80%" label="用户名">
                        <el-input type="text" v-model="Form.username" autofocus ref="username" auto-complete="off"
                                  placeholder="请输入要找回密码的用户名" prefix-icon="el-icon-user-solid" spellcheck="false" :disabled="isUsername">
                        </el-input>
                    </el-form-item>
                    <el-form-item style="float: right; width: 80%" label="邮箱号">
                        <el-input type="text" v-model="Form.email" autofocus ref="email" auto-complete="off"
                                  placeholder="请输入用来找回密码的邮箱" prefix-icon="el-icon-message" spellcheck="false" :disabled="!isUsername">
                        </el-input>
                    </el-form-item>
                </el-form>
            </div>
            <div v-if="active === 1" class="common_div">
                <el-form :model="codeForm"  class="user-container" label-position="left" label-width="60px" size="medium">
                    <el-form-item  style="float: right; width: 80%" label="验证码">
                        <el-input type="text" v-model="codeForm.code" autofocus ref="code" auto-complete="off"
                                  placeholder="请输入邮箱验证码" prefix-icon="el-icon-s-promotion" spellcheck="false">
                        </el-input>
                    </el-form-item>
                </el-form>
            </div>
            <div v-if="active === 2" class="common_div">
                <el-form :model="passwordForm"  class="user-container" label-position="left" label-width="60px" size="medium">
                    <el-form-item  style="float: right; width: 80%" label="新密码">
                        <el-input type="password" v-model="passwordForm.password" autofocus ref="password"
                                  auto-complete="off" placeholder="请输入新密码" prefix-icon="el-icon-key" >
                        </el-input>
                    </el-form-item>
                </el-form>
            </div>
            <div class="common_div">
                <el-button  @click="next" :disabled="disabled" class="action_button">下一步</el-button>
            </div>

        </div>
    </div>
</template>

<script>
export default {
    name: "ResetPassword",

    data() {
        return {
            active: 0,
            Form: {
                username: '',
                email: '',
            },
            codeForm: {
                code: '',
            },
            passwordForm: {
                password: '',
            },
            disabled: false,
            isUsername: false,
        }
    },
    created() {
        let regEmail = /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+/;
        if (regEmail.test(this.$route.query.username)) {
            console.log("传来了邮箱")
            this.Form.email = this.$route.query.username;
            this.isUsername = false;
        } else {
            // 传来的不是邮箱,那就是用户名
            console.log("传来了用户名")
            this.Form.username = this.$route.query.username;
            this.isUsername = true;
        }

    },
    methods: {
        isEmail() {
            let regEmail = /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+/;
            if (!regEmail.test(this.Form.email)) {
                this.$message({
                    message: '邮箱格式不正确',
                    type: 'error'
                });
                return false;
            }
            return true;
        },
        beforePost () {
            if (this.Form.username === '' || this.Form.email === '') {
                // 重置密码时,如果用户名或者邮箱未输入,那么提醒用户输入
                if (this.Form.username === '') {
                    this.$message({
                        message: '警告, 用户名未输入哦',
                        type: 'warning'
                    });
                    this.$refs.username.focus();
                }else {
                    this.$message({
                        message: '警告, 邮箱未输入呀',
                        type: 'warning'
                    });
                    this.$refs.email.focus();
                }
                return false;
            }else {
                //进行邮箱格式的检测
                return this.isEmail();
            }
        },
        next() {
            // 当面板为0时,先判断用户名和邮箱是否输入,进行相关的验证
            if (this.active === 0) {
                let isFinished = this.beforePost();
                if (isFinished) {
                    //数据输入正确后,将按钮禁掉,并提示相关信息,然后数据发送到后台
                    this.disabled = true;
                    this.$notify.info({
                        title: '提示',
                        message: '数据正确发送,请耐心等待,勿重复操作!',
                        duration: 0,
                        offset: 100,
                    });
                    this.post();
                }
            }
            // 当面板为1时,则到了用户输入验证码的时候, 将验证码传入后台
            if (this.active === 1) {
                // 如果验证码未输入,提示用户
                if (this.codeForm.code === '') {
                    this.$notify({
                        title: '警告',
                        message: '警告, 验证码未输入,请去您邮箱中查看!',
                        type: 'warning',
                        offset: 100,
                    });
                    this.$refs.code.focus();
                }else {
                    this.$axios.post('/resetPassword', {
                        code: this.codeForm.code,
                        username: this.username,
                    }).then(successResponse => {
                        if (successResponse.data.code === 200) {
                            //验证码输入正确,
                            this.active++;
                            this.$notify({
                                title: '成功',
                                message: '验证码匹配正确!',
                                type: 'success',
                                duration: 0,
                                offset: 100,
                            });
                        }else if (successResponse.data.code === 400) {
                            //验证码匹配错误返回对应信息
                            this.$message.error(successResponse.data.message);
                        }
                    }).catch(failResponse => {

                    })
                }

            }
            // 当面板为2时,则到了用户输入密码的时候, 将密码传入后台
            if (this.active === 2) {
                //在发送密码之前,先校验一下是否输入了,不能让用户不小心输入了空密码
                if (this.passwordForm.password === '') {
                    this.$notify({
                        title: '警告',
                        message: '警告, 新密码未输入',
                        type: 'warning',
                        offset: 100,
                    });
                    this.$refs.password.focus();
                }else {
                    let password_md5 = this.$md5(this.passwordForm.password);
                    this.$axios.post('/resetPassword', {
                        password: password_md5,
                        username: this.username,
                    }).then(successResponse => {
                        if (successResponse.data.code === 200) {
                            //密码修改成功
                            this.$notify({
                                title: '成功',
                                message: '该账号密码修改正确!',
                                type: 'success',
                                duration: 0,
                                offset: 100,
                            });
                            let path = this.$route.query.redirect;
                            this.$router.replace({path: path === '/' || path === undefined ? '/login' : path})
                        }else if (successResponse.data.code === 400) {
                            //修改密码失败,返回对应信息
                            this.$message.error(successResponse.data.message);
                        }
                    }).catch(failResponse => {

                    })
                }
            }
        },
        post(){
            console.log(this.Form.username)
            console.log(this.Form.email)
            this.$axios
                .post('/resetPassword', {
                    username: this.Form.username,
                    email: this.Form.email,
                })
                .then(successResponse => {
                    if (successResponse.data.code === 200) {
                        // 如果返回的结果正确,那么需要发送邮件到对应的用户邮箱中,用户自己登录邮箱后找到对应的链接后才可以输入新密码
                        this.$notify({
                            title: '成功',
                            message: '已向'+ this.Form.email + '发送验证码,请在5分钟之内修改密码,否则验证码失效',
                            type: 'success',
                            duration: 0,
                            offset: 100
                        });
                        //跳转到下一个面板,并且将按钮恢复正常
                        this.active++;
                        this.disabled = false;
                        //将用户username保存下来
                        this.username = successResponse.data.result;
                    }else if (successResponse.data.code === 400) {
                        //如果用户名和密码匹配错误,那么显示错误信息,并让按钮重新可用
                        this.$notify({
                            title: '失败',
                            message: successResponse.data.message + "未知错误!",
                            type: 'error',
                            duration: 0,
                            offset: 100,
                        });
                        this.disabled = false;
                    }
                })
                .catch(failResponse => {
                })
        },
    }
}
</script>

<style scoped>
    .resetPassword{
        background-image: url("../../assets/resetPassword_img.jpg");
        background-position: center;
        height: 100%;
        width: 100%;
        background-size: cover;
        position: fixed;
    }
    .container{
        border-radius: 15px;
        background-clip: padding-box;
        margin: 10% auto;
        width: 30%;
        padding: 25px 30px;
        background: #fff;
        border: 1px solid #eaeaea;
        box-shadow: 0 0 25px #cac6c6;
        opacity: 0.7;
    }
    .common_div{
        margin-top: 5%;
    }
    .user-container {
        width: 80%;
        background: #fff;

    }
    .action_button {
        width: 20%;
        margin-top: 3%;
        text-align: center;
    }

</style>

代码解释:

  • el-steps: element组件中的步骤条,详细使用查看 element 步骤条。
  • Form: 表单数据对象。
  • v-if: 条件渲染,分步骤。
  • 其他函数详情注释。

界面效果展示

二、后端代码设计

1. JavaMail配置

要想在springboot中发送邮件,需要提供特定的依赖。首先是mail依赖,然后是lombok依赖,和mybatis-plus依赖,负责数据库字段和实体属性的映射。在pom.xml中添加依赖:

		<!-- 邮件依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        
         <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!--mybatis-plus自动的维护了mybatis以及mybatis-spring的依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>

2. QQ邮箱开启STMP授权

Vue + element + Springboot 通过邮箱找回密码_第2张图片
开启后会获得一个邮箱 授权码,这个授权码可以用记事本记录下来。

注:网易邮箱之前申请时候无法使用,推荐使用qq邮箱发送。

3. 配置applicaiton.yml文件

spring:
  mail:
    username: 填写你的qq邮箱
    password: 填写qq邮箱授权码
    host: smtp.qq.com
    protocol: smtp
    default-encoding: UTF-8
    properties:
      mail.smtp.auth: true
      mail.smtp.starttls.enable: true
      mail.smtp.starttls.required: true
      mail.smtp.socketFactory.port: 465 #协议为SMTP是SSL端口号465
      mail.smtp.socketFactory.class: javax.net.ssl.SSLSocketFactory
      mail.smtp.socketFactory.fallback: false

4. 新建文件夹

config、controller、entity、mapper、result、service

其他可以忽略。

整个项目文件夹显示如下
Vue + element + Springboot 通过邮箱找回密码_第3张图片
解释:

  • config:存储相关的配置,如邮件配置,数据库配置,shiro配置等。
  • controller:控制器。
  • entity: 实体类
  • exception: 异常类,此处无需理会。
  • filter: 过滤器,此处无需理会.
  • mapper: 数据库基础接口,管理CRUD操作。
  • realm: shiro控制登录信息。
  • result:返回前端信息类。
  • service: 接口和实例层, 主要的业务逻辑层。
  • utils:一些封装的方法。
  • impl:实例。
  • service: 接口。
    在这里插入图片描述

5. 邮件配置:

EmailConfig.java

package com.lkq.pet.config;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;


/**
 * @author GoldenRetriever
 * @time 2020/10/12 16:57
 * @description 邮箱配置
 */
@Data
@Component
public class EmailConfig {
    /**
     * 发件人邮箱
     */
    @Value("${spring.mail.username}")
    private String emailForm;
}

EmailService.java

package com.lkq.pet.service;



/**
 * @author GoldenRetriever
 * @time 2020/10/12 16:59
 * @description 邮件服务接口,该实体用于用户重置密码
 */
public interface EmailService {

    /**
     * 发送简单邮件
     * @param sendTo 收件人地址
     * @param title  邮件标题
     * @param content 邮件内容
     */
    void sendSimpleMail(String sendTo, String title, String content);

    /**
     * 发送HTML邮件
     * @param sendTo 收件人地址
     * @param title 邮件标题
     * @param content 邮件内容
     */
    void sendHtmlMail(String sendTo, String title, String content);
}

EmailServiceImpl.java

package com.lkq.pet.service.impl;

import com.lkq.pet.config.EmailConfig;
import com.lkq.pet.service.EmailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;

import javax.mail.internet.MimeMessage;

/**
 * @author GoldenRetriever
 * @time 2020/10/12 17:03
 * @description 邮件业务层实例,实现对应接口方法
 */
@Service
public class EmailServiceImpl implements EmailService {

    @Autowired
    private EmailConfig emailConfig;

    @Autowired
    private JavaMailSender mailSender;

    /**
     * 发送简单邮件后端耗时明显更短,
     * @param sendTo 收件人地址
     * @param title  邮件标题
     * @param content 邮件内容
     */
    @Override
    public void sendSimpleMail(String sendTo, String title, String content) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(emailConfig.getEmailForm());
        message.setTo(sendTo);
        message.setSubject(title);
        message.setText(content);
        try{
            mailSender.send(message);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 发送Html邮件时间将会变长,但是需求更多(暂时带有附件的功能未添加)
     * @param sendTo 收件人地址
     * @param title 邮件标题
     * @param content 邮件内容
     */
    @Override
    public void sendHtmlMail(String sendTo, String title, String content) {
        MimeMessage message = mailSender.createMimeMessage();

        try{
            //true表示需要创建一个multipart message
            MimeMessageHelper helper = new MimeMessageHelper(message, true);
            helper.setFrom(emailConfig.getEmailForm());
            helper.setTo(sendTo);
            helper.setSubject(title);
            helper.setText(content, true);
            mailSender.send(message);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

6. User相关类:User.java、UserMapper、UserService.java、UserServiceImpl.java

User对应实体,UserMapper连接数据库,UserService.java对应封装的接口方法,UserServiceImpl继承UserService,实现接口方法。

User.java

package com.lkq.pet.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;
import org.springframework.stereotype.Repository;

import java.util.Date;


/**
 * @author GoldenRetriever
 * @time 2020/10/5 20:39
 * @description 用户实体类
 */
@Data
@TableName(value = "user")
@Repository
public class User {
    /**
     * 主键user_id,用户名,密码(存错加密密文),盐(加密盐),性别,真实姓名,生日,头像,邮箱,角色id, 用户状态
     */
    @TableId(value = "user_id", type = IdType.AUTO)
    private Integer userId;

    @TableField(value = "username")
    private String username;

    @TableField(value = "password")
    private String password;

    @TableField(value = "salt")
    private String salt;

    @TableField(value = "gender")
    private int gender;

    @TableField(value = "real_name")
    private String realName;

    // 返回时间格式
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
    @TableField(value = "birthday")
    private Date birthday;

    @TableField(value = "avatar")
    private String avatar;

    @TableField(value = "email")
    private String email;

    @TableField(value = "state")
    private int state;

    @TableField(exist = false)
    private String code;


}

UserMapper.java

package com.lkq.pet.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lkq.pet.entity.User;

/**
 * @author LKQ
 * @date 2021/3/29 9:16
 * @description
 */
public interface UserMapper extends BaseMapper<User> {
}

UserService.java


package com.lkq.pet.service;


import com.baomidou.mybatisplus.extension.service.IService;
import com.lkq.pet.entity.Role;
import com.lkq.pet.entity.User;
import com.lkq.pet.result.Result;

import java.util.List;
import java.util.Map;

/**
 * @author GoldenRetriever
 * @time 2020/10/6 10:34
 * @description 继承mybatis-plus提供的IService接口,进一步封装CRUD,具体方法看官网
 */
public interface UserService extends IService<User> {
    /**
     * 数据库中是否存在用户名
     * @param username 用户名
     * @return boolean
     */
    boolean isExistUser(String username);
	
	/**
     * 通过用户名查找对应的邮箱号
     * @param username 用户名
     * @return email 邮箱号
     */
   	String findEmailByUsername(String username);
    
     /**
     * 通过用户名查找对应的用户id
     * @param username 用户名
     * @return id
     */
    int findIdByUsername(String username);
}

UserServiceImpl.java

package com.lkq.pet.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lkq.pet.entity.*;
import com.lkq.pet.mapper.UserMapper;
import com.lkq.pet.result.Result;
import com.lkq.pet.result.ResultFactory;
import com.lkq.pet.service.*;
import com.lkq.pet.utils.Md5Utils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Service;
import org.springframework.web.util.HtmlUtils;

import javax.annotation.Resource;
import java.util.*;

/**
 * @author GoldenRetriever
 * @time 2020/10/6 10:35
 * @description UserService层实现UseService接口,继承了mybatis-plus提供的ServiceImpl类
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{

    @Resource
    private UserService userService;

    @Resource
    private ResetPasswordService resetPasswordService;

    @Resource
    private EmailService emailService;

    /**
     * 检查想重置密码的用户和邮箱是否匹配,若匹配成功,则返回当前用户的相关信息
     * @param user 用户实体
     * @return Result信息
     */
    @Override
    public Result checkUserAndEmail(User user) {
        //获取前端传来的数据
        String username = user.getUsername();
        String email = user.getEmail();
        //将用户名中可能存在HTML编码转义
        username = HtmlUtils.htmlEscape(username);
        email = HtmlUtils.htmlEscape(email);
        if (isExistUser(username)) {
            //如果存在该用户,则查找该用户的邮箱及用户id
            String storeEmail = findEmailByUsername(username);
            int userId = findIdByUsername(username);
            if (email.equals(storeEmail)) {
                //用户名和邮箱匹配正确后,需要在表resetPassword中生成一条信息,保存验证码,修改次数等信息
                //生成一个6位的数字
                int num = (int) ((Math.random() * 9 + 1) * 100000);
                String code = String.valueOf(num);
                //当前时间作为修改密码的开始时间
                Date currentTime = new Date();
                //截至时间为开始时间延后5分钟
                Date deadline = new Date(currentTime.getTime() + 5*60*1000);
                int limitNum = 3;
                int isEffective = 1;
                if (resetPasswordService.isExistUserId(userId)) {
                    //如果表中存在这一个用户的修改信息,那么只需要修改相关信息
                    ResetPassword rp = resetPasswordService.getOneByUserId(userId);
                    if (currentTime.after(new Date(rp.getCreateTime().getTime() + 24*60*60*1000))){
                        //如果当前的时间在修改密码存储时间后一天,那么就判断过了冷却时间,该用户可以重新设置密码, 24小时60分钟60s
                        rp.setIsEffective(isEffective);
                        rp.setResetNum(0);
                    }
                    if (rp.getResetNum() > rp.getLimitNum()) {
                        //如果当前修改次数超过上限
                        rp.setIsEffective(0);
                    }
                    //先判断用户是否能够修改密码, 值为1代表允许,否则不能修改
                    if (rp.getIsEffective() == 1) {
                        // 设置对应的数据
                        rp.setCode(code);
                        rp.setCreateTime(currentTime);
                        rp.setDeadline(deadline);
                        //重置次数+1
                        int resetTimes = rp.getResetNum();
                        resetTimes++;
                        rp.setResetNum(resetTimes);
                        rp.setLimitNum(limitNum);
                        if (rp.getResetNum() > rp.getLimitNum()) {
                            // 修改次数 > 限制次数
                            rp.setIsEffective(0);
                        }else {
                            rp.setIsEffective(isEffective);
                        }
                    }else {
                        return ResultFactory.buildFailResult("当日账号密码修改次数超过上限,请明天重试!");
                    }
                    try{
                        resetPasswordService.updateById(rp);
                    }catch (Exception e) {
                        System.out.println(e);
                    }
                }else {
                    //表中不存在这个用户,那么需要重新添加一条新数据
                    ResetPassword rp = new ResetPassword();
                    rp.setUserId(userId);
                    rp.setCode(code);
                    rp.setCreateTime(currentTime);
                    rp.setDeadline(deadline);
                    rp.setIsEffective(isEffective);
                    rp.setResetNum(0);
                    rp.setLimitNum(limitNum);
                    try{
                        resetPasswordService.save(rp);
                    }catch (Exception e) {
                        System.out.println(e);
                    }
                }
                //发送html邮件到对应的邮箱号
                String title = "重置密码-来自lkq宠物医院管理后台";
                String content = "\n" +
                        "\n" +
                        "

hello! 忘记密码啦?!

\n"
+ "

" + "用户" + username + ": 你好"+"
"
+ "你正在lkq宠物医院平台进行重置密码操作
"
+ "您本次重置密码的验证码为
"
+ "

" + code + "

"
+ "
请在5分钟之内填写验证码"
+ "
如果非本人操作,请忽略本邮件, 如有疑问,欢迎致信[email protected]"
+ "

"
+ "\n" + "\n"; //发送Html邮件时间相对较长 emailService.sendHtmlMail(email, title, content); return ResultFactory.buildSuccessResult(username); } return ResultFactory.buildFailResult("用户邮箱号输入错误,请重新输入"); } return ResultFactory.buildFailResult("该用户未注册,请先注册账号"); } /** * 检查验证码是否正确 * @param code 验证码 * @param username 用户username * @return Result信息 */ @Override public Result checkCode(String code, String username) { int userId = findIdByUsername(username); ResetPassword rp = resetPasswordService.getOneByUserId(userId); //获取当前时间 Date currentTime = new Date(); if (currentTime.after(rp.getDeadline())) { return ResultFactory.buildFailResult("验证时间已过,请刷新界面,从头开始重置密码!"); }else { if (rp.getCode().equals(code)) { return ResultFactory.buildSuccessResult("验证码匹配正确", username); } return ResultFactory.buildFailResult("验证码匹配错误!"); } } /** * 重置密码 * @param password 输入密码的md5密文 * @param username 用户名 * @return Result信息 */ @Override public Result resetPassword(String username, String password) { int userId = findIdByUsername(username); User user = userService.getById(userId); //获取随机的16位长度盐 String salt = Md5Utils.getSalt(); user.setSalt(salt); //md5加密后的密码和随机生成的salt拼接后再加密形成第二密文 user.setPassword( Md5Utils.getSaltMd5(password, salt)); //更新到数据库 userService.updateById(user); return ResultFactory.buildSuccessResult("修改密码成功"); } /** * 判断数据库中是否存在用户 * @param username 用户名 * @return true、false */ @Override public boolean isExistUser(String username) { //mybatis-plus的条件构造器queryWrapper QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); return userService.count(queryWrapper) > 0; } /** * 通过用户名查找对应的邮箱号 * @param username 用户名 * @return email 邮箱号 */ @Override public String findEmailByUsername(String username) { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); //找到数据库中用户名和输入用户名相同的一条数据 User user = userService.getOne(queryWrapper); //返回对应的邮箱 return user.getEmail(); } /** * 通过用户名查找对应的用户id * @param username 用户名 * @return id */ @Override public int findIdByUsername(String username) { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); //找到数据库中用户名和输入用户名相同的一条数据 User user = userService.getOne(queryWrapper); //返回对应的用户id return user.getUserId(); } }

7. ResetPassword相关类

ResetPassword实体类,ResetPasswordMapper 基础映射, ResetPasswordService接口方法,ResetPassImpl实现接口方法。

ResetPassword.java

package com.lkq.pet.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.stereotype.Repository;

import java.util.Date;

/**
 * @author GoldenRetriever
 * @time 2020/10/12 19:53
 * @description 重置密码实体,对应resetPassword表
 */
@Data
@TableName(value = "resetPassword")
@Repository
public class ResetPassword {

    /**
     * 主键id
     */
    @TableId(value = "id", type = IdType.AUTO)
    private int id;

    /**
     * 重置用户密码的用户id
     */
    @TableField(value = "user_id")
    private int userId;

    /**
     * 随机生成的16位验证码
     */
    @TableField(value = "code")
    private String code;

    /**
     * 开始时间
     */
    @TableField(value = "create_time")
    private Date createTime;

    /**
     * 截至时间
     */
    @TableField(value = "deadline")
    private Date deadline;

    /**
     * 是否有效,若当前时间超出截至时间,则判定当前验证码无效,0代表无效,1代表有效
     */
    @TableField(value = "is_effective")
    private int isEffective;

    /**
     * 重置次数,记录当前重置次数
     */
    @TableField(value = "reset_num")
    private int resetNum;

    /**
     * 当日限定重置次数, 默认为3次
     */
    @TableField(value = "limit_num")
    private int limitNum;

}

ResetPasswordMapper.java

package com.lkq.pet.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lkq.pet.entity.ResetPassword;

/**
 * @author LKQ
 * @date 2021/3/29 9:11
 * @description
 */
public interface ResetPasswordMapper extends BaseMapper<ResetPassword> {
}

ResetPasswordService.java

package com.lkq.pet.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.lkq.pet.entity.ResetPassword;

/**
 * @author GoldenRetriever
 * @time 2020/10/12 22:18
 * @description 定义重置密码的接口类,
 */
public interface ResetPasswordService extends IService<ResetPassword> {
    /**
     * 通过userId判断resetPassword表中该用户是否修改过密码,有没有数据
     * @param userId 用户id
     * @return boolean
     */
    boolean isExistUserId(int userId);

    /**
     * 通过userId取出这一条数据
     * @param userId 用户id
     * @return resetPassword对象
     */
    ResetPassword getOneByUserId(int userId);
}

ResetPassImpl.java

package com.lkq.pet.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lkq.pet.entity.ResetPassword;
import com.lkq.pet.mapper.ResetPasswordMapper;
import com.lkq.pet.service.ResetPasswordService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * @author GoldenRetriever
 * @time 2020/10/12 22:25
 * @description 重置密码的实现类
 */
@Service
public class ResetPassImpl extends ServiceImpl<ResetPasswordMapper, ResetPassword> implements ResetPasswordService {

    @Resource
    private ResetPasswordService resetPasswordService;

    /**
     * 判断是否存在该用户修改的数据,
     * @param userId 用户id
     * @return boolean
     */
    @Override
    public boolean isExistUserId(int userId) {
        QueryWrapper<ResetPassword> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_Id", userId);
        return resetPasswordService.count(queryWrapper) > 0;
    }

    /**
     * 通过用户id获取这条数据
     * @param userId 用户id
     * @return ResetPassword实例
     */
    @Override
    public ResetPassword getOneByUserId(int userId) {
        QueryWrapper<ResetPassword> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_Id", userId);
        return resetPasswordService.getOne(queryWrapper);
    }
}

8. 数据库创建表格

  • user:存储用户信息,这里主要用到user_id。
  • resetpassword:根据上面实体创建表。

9. 编写控制器LoginController

package com.lkq.pet.controller;


import com.lkq.pet.result.Result;
import com.lkq.pet.entity.User;
import com.lkq.pet.result.ResultFactory;
import com.lkq.pet.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;


/**
 * @author GoldenRetriever
 * @time 2020/10/5 21:47
 * @description 后端登录控制器,处理前端请求
 */
@RestController
@CrossOrigin(value = "http://localhost:8080", maxAge = 1800, allowedHeaders ="Content-Type")
public class LoginController {

    @Autowired
    private UserServiceImpl userServiceImpl;
    
    @PostMapping("/api/resetPassword")
    public Result resetPassword(@RequestBody User user) {
        if (user.getUsername()!=null&& user.getEmail()!=null){
            //第一步,传来的是用户名和邮件,其他为空,则生成验证码并发送邮件
            return userServiceImpl.checkUserAndEmail(user);
        }
        if (user.getCode()!=null && user.getUsername()!=null) {
            //第二步,传来code和username,需要验证数据库中的code是否正确
            return userServiceImpl.checkCode(user.getCode(), user.getUsername());
        }
        if (user.getUsername()!=null && user.getPassword()!=null) {
            //最后,用户名和密码同时传过来,开始重置密码。
            return userServiceImpl.resetPassword(user.getUsername(), user.getPassword());
        }
        return ResultFactory.buildFailResult("未知错误");
    }

}

@CrossOrigin注解:用来跨域。

10. Result类

这个类的作用是为了处理后端返回数据,项目文件夹:

Vue + element + Springboot 通过邮箱找回密码_第4张图片
Result.java

package com.lkq.pet.result;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

/**
 * @author GoldenRetriever
 * @time 2020/9/18 21:46
 * @description 存储响应结果
 */
@Data
@Component
@NoArgsConstructor
public class Result {
    /**
     * 响应码,结果信息,数据
     */
    private int code;
    private String message;
    private Object result;

    Result(int code, String message, Object data) {
        this.code = code;
        this.message = message;
        this.result = data;
    }

}

ResultCode.java

package com.lkq.pet.result;

/**
 * @author GoldenRetriever
 * @time 2020/10/7 15:01
 * @description ResultCode类
 */
public class ResultCode {
    /**
     * Http状态码, 200请求成功,400客户端请求语法错误,
     * 401请求要求用户的身份认证
     * 404服务器无法根据客户端的请求找到资源(网页),
     * 500服务器内部错误,无法完成请求
     */
    static int SUCCESS = 200;
    static int FAIL = 400;
    public static int UNAUTHORIZED = 401;
    public static int NOTFOUND =404;
    public static int INTERNAL_SERVER_ERROR = 500;

    public int code;

    ResultCode(int code) {
        this.code = code;
    }

}

ResultFactory.java

package com.lkq.pet.result;

/**
 * @author GoldenRetriever
 * @time 2020/10/7 15:11
 * @description
 */
public class ResultFactory {

    public static Result buildResult(int resultCode, String message, Object data) {
        return new Result(resultCode, message, data);
    }

    /**
     * 连接错误
     * @param message 错误信息
     * @return Result
     */
    public static Result buildFailResult(String message) {
        return buildResult(ResultCode.FAIL, message, null);
    }

    /**
     * 连接成功
     * @param data 返回数据
     * @return Result
     */
    public static Result buildSuccessResult(Object data) {
        return buildResult(ResultCode.SUCCESS, "成功", data);
    }

    /**
     * 操作成功
     * @param message 提示信息
     * @param data 传回数据
     * @return Result对象
     */
    public static Result buildSuccessResult(String message, Object data) {
        return buildResult(ResultCode.SUCCESS, message, data);
    }
}

效果展示

一、登录界面

二、找回密码


这里从登录界面传来了用户名:最好的一天,且无法修改。

三、输入邮箱


下一步之后

四、输入验证码


网易邮箱收到验证码。
Vue + element + Springboot 通过邮箱找回密码_第5张图片
验证码在限定时间内匹配成功

五、修改密码

输入新密码后

数据库变化

其中214968就是发送的验证码
在这里插入图片描述

结尾

天空好想下雨,我好想住你隔壁!

查漏补缺

2021/7/29

针对留言中提出的UserServiceImpl类中resetPassword方法找不到引用类,原因是博主之前后端生成随机16位加密盐是手动写的方法,在之前的开发中能够成功。后来因为使用到shiro框架,于是就将这部分给优化了,调用官方提供的方法来生成随机盐。具体是先导入在该类头部导入两个包,然后修改resetPassword方法,应该就能够解决问题。

import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
 	/**
     * 重置密码
     * @param password 输入密码的md5密文
     * @param username 用户名
     * @return Result信息
     */
    @Override
    public Result resetPassword(String username, String password) {
        int userId = findUserIdByUsername(username);
        try {
            User user = userService.getById(userId);
            //获取随机的16位长度盐
            String salt = new SecureRandomNumberGenerator().nextBytes().toString();
            user.setSalt(salt);
            int times = 2;
            //md5加密后的密码和随机生成的salt拼接后再加密形成第二密文
            user.setPassword(new SimpleHash("md5", password, salt, times).toString());
            //更新到数据库
            userService.updateById(user);
        }catch (Exception e) {
            e.printStackTrace();
            return ResultFactory.buildFailResult("未知错误");
        }
        return ResultFactory.buildSuccessResult("修改密码成功");
    }

你可能感兴趣的:(vue,+,springboot,vue,java,vue.js,mysql)