接上一篇登录界面 拼图验证, 项目开发过程中还有一个需求,实现 通过邮箱重置密码。
重置密码,作为一个正经网站,那都是必备的需求,那咱可不得整一个!
但是鉴于设计的范围有点广,先列出我觉得你需要会一点的东西,否则,直接dang代码可能报很多错误。
前端知识:
后端知识:
1
. 根据用户注册时输入的邮箱账号
,当进入重置密码界面时,先输入用户名和邮箱,前端表单将输入送到后端时,判断数据库中是否有对应的账户及邮箱
2
. 如果存在,则后端发送一份携带验证码
的html邮件
到用户邮箱,用户复制验证码到前端页面
3
. 提交验证码到后端
,后端根据用户名和验证码验证是否匹配,若匹配成功,则进入到输入密码的板块;若过期,则从 1
开始重置密码。
4
. 最后将当前的用户名密码重置为新密码。
5
. 一个账号一天之内只能重置3
次,超出后当天不在让其重置密码。
前端项目配置,首先需要安装axios, 用来跨域传输数据,以及element,因为绘制组件的时候用到了,这里我就默认你们都有了。
为什么要登录界面呢?
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来存储登录信息,不是重点。找回密码
:利用邮箱找回密码,重点。界面效果展示:
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:
条件渲染,分步骤。界面效果展示
要想在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>
开启后会获得一个邮箱 授权码
,这个授权码可以用记事本记录下来。
注:网易邮箱之前申请时候无法使用,推荐使用qq邮箱发送。
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
config、controller、entity、mapper、result、service
其他可以忽略。
config
:存储相关的配置,如邮件配置,数据库配置,shiro配置等。controller
:控制器。entity
: 实体类exception
: 异常类,此处无需理会。filter
: 过滤器,此处无需理会.mapper
: 数据库基础接口,管理CRUD操作。realm
: shiro控制登录信息。result
:返回前端信息类。service
: 接口和实例层, 主要的业务逻辑层。utils
:一些封装的方法。impl
:实例。service
: 接口。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();
}
}
}
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();
}
}
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);
}
}
user
:存储用户信息,这里主要用到user_id。resetpassword
:根据上面实体创建表。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注解:用来跨域。
这个类的作用是为了处理后端返回数据,项目文件夹:
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);
}
}
这里从登录界面传来了用户名:最好的一天,且无法修改。
下一步之后
输入新密码后
数据库变化
其中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("修改密码成功");
}