SpringBoot实现通过邮箱找回密码功能

简介: SpringBoot实现通过邮箱找回密码功能

1.前言


之前在大学里面做项目的时候碰到修改密码那一块的,自己当时都是做的很简单的逻辑,也想过怎么通过邮箱或者手机号这种进一步验证身份来修改密码,但是自己当时太菜了,也没怎么好好钻研,所以就一直没尝试过那样的功能,但是这次公司项目里面可能会用到,于是自己找了找教程看了看,发现实现起来不难,毕竟别人已经把轮子已经造好了,但是其中还是遇到了一些问题,还是费了不少时间.希望这篇教程能过对你有所帮助.


2.步骤


2.1导入依赖

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


导入这个依赖就够了


2.2开启邮箱的SMTP服务


这里我以 QQ邮箱 举例,其他的邮箱可以自行查看教程


20201026152310393.png


这里开启比较简单中途就是需要我们发送短信去验证我们的身份


20201026152440153.png


之后我们还需要去开启我们的授权码


20201026152528674.png


这个过程也需要我们发送短信验证我们的身份,验证之后就会生成我们的授权码


20201026152658943.png


可以看到下面的小子,授权码可以有多个,如果忘记了,我们可以重新过来验证身份生成授权码,所以不用担心记不住授权码.


2.3配置application.yaml文件

#发送邮件
spring:
  mail:
    #这个需要根据你开启的服务的邮箱是QQ邮箱,还是网易邮箱区分
    #smtp.163.com为网易邮箱,smtp.qq.com为QQ邮箱
    host: smtp.qq.com
    username: 2293557957@qq.com #你开通SMTP服务的邮箱账号
    password: ........ #刚刚生成的授权码,不是邮箱密码
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
            required: true
    default-encoding: UTF-8


配置完发送邮件的参数之后,我们就能来编写逻辑了.


2.4 编写逻辑的步骤


在编写逻辑之前我们先来梳理一下我们通过邮箱找回密码的整体流程


20201026154738590.png


这是简化的逻辑,现在我们将它进行细化

主要思想如下图所示:


20201026163745926.png


了解了整个流程之后,我们来编写


2.4.1创建pm_validate


DROP TABLE IF EXISTS `pm_validate`;
CREATE TABLE `pm_validate` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `email` varchar(40) NOT NULL,
  `reset_token` varchar(40) NOT NULL,
  `type` varchar(20) NOT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


该表主要是记录用户请求发送邮件时生成的Token信息,以及产生的时间,方便判断链接是否以及失效


2.4.2创建对应的实体类以及mapper文件


实体对象


需要引入lombok


<dependency>
   <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.20</version>
    <scope>provided</scope>
</dependency>
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ValidateDao {
    private Integer id;
    private Integer userId;
    private String email;
    private String resetToken;
    private String type;
    private Date gmtCreate;
    private Date gmtModified;
}


mapper.xml文件

这里之后的实体对象的路径记得修改成自己路径下的路径


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="ams.web.admin.dao.ValidateDaoMapper">
    <resultMap id="BaseResultMap" type="ams.web.admin.entity.ValidateDao">
        <id column="id" jdbcType="INTEGER" property="id" />
        <result column="user_id" jdbcType="INTEGER" property="userId" />
        <result column="email" jdbcType="VARCHAR" property="email" />
        <result column="reset_token" jdbcType="VARCHAR" property="resetToken" />
        <result column="type" jdbcType="VARCHAR" property="type" />
        <result column="gmt_create" jdbcType="TIMESTAMP" property="gmtCreate" />
        <result column="gmt_modified" jdbcType="TIMESTAMP" property="gmtModified" />
    </resultMap>
    <sql id="Base_Column_List">
        id,user_id,email,reset_token,type,gmt_create,gmt_modified
    </sql>
    <select id="selectByToken" parameterType="java.lang.String" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from pm_validate
        where reset_token = #{token,jdbcType=VARCHAR}
    </select>
    <select id="selectByEmail" parameterType="java.lang.String" resultMap="BaseResultMap">
        select <include refid="Base_Column_List"/>
        from pm_validate
        where email=#{email,jdbcType=VARCHAR}
    </select>
    <insert id="insert" parameterType="ams.web.admin.entity.ValidateDao">
        insert into pm_validate (user_id,email,reset_token,type,gmt_create,gmt_modified)
        values (#{userId,jdbcType=VARCHAR},#{email,jdbcType=INTEGER},#{resetToken,jdbcType=INTEGER},#{type,jdbcType=VARCHAR}, #{gmtCreate,jdbcType=TIMESTAMP}, #{gmtModified,jdbcType=TIMESTAMP}
               )
    </insert>
    <insert id="insertSelective" parameterType="ams.web.admin.entity.ValidateDao">
        insert into pm_validate
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="userId != null">
                user_id,
            </if>
            <if test="email != null">
                email,
            </if>
            <if test="rest_token != null">
                rest_token,
            </if>
            <if test="type != null">
                type,
            </if>
            <if test="gmtCreate != null">
                gmt_create,
            </if>
            <if test="gmtModified != null">
                gmt_modified,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="userId != null">
                #{user_id,jdbcType=INTEGER},
            </if>
            <if test="email != null">
                #{email,jdbcType=VARCHAR},
            </if>
            <if test="rest_token != null">
                #{rest_token,jdbcType=VARCHAR},
            </if>
            <if test="type != null">
                #{type,jdbcType=VARCHAR},
            </if>
            <if test="gmtCreate != null">
                #{gmt_create,jdbcType=TIMESTAMP},
            </if>
            <if test="gmtModified != null">
                #{gmt_modified,jdbcType=TIMESTAMP},
            </if>
        </trim>
    </insert>
</mapper>


2.4.3编写 dao


public interface ValidateDaoMapper {
    int insert(ValidateDao validateDao);
    ValidateDao selectByToken(String token);
    List<ValidateDao>selectByEmail(String email);
}

2.4.4编写 service


主要包括以下方法

  • 发送邮件
  • 向validate表中插入我们生成的用户Token信息的数据
  • 通过Token查找记录
  • 判断是否发送重置密码的邮件
  • 判断链接是否失效
@Service
public class ValidateService{
    @Autowired
    private JavaMailSender javaMailSender;
    @Autowired
    private ValidateDaoMapper validateDaoMapper;
    /**
     * 发送邮件:@Async进行异步调用发送邮件接口
     * @param email
     */
//    @Async
//    public void sendPasswordResetEmail(SimpleMailMessage email){
//
//        javaMailSender.send(email);
//    }
    @Async
    public void sendPasswordResetEmail(MimeMessage email){
        javaMailSender.send(email);
    }
    /**
     * 在pm_validate表中插入一条validate记录,userid,email属性来自pm_user表,token由UUID生成
     * @param validateDao
     * @param userDao
     * @param token
     * @return
     */
    public int insertNewResetRecord(ValidateDao validateDao, UserDao userDao, String token){
        validateDao.setUserId(userDao.getUserId());
        validateDao.setEmail(userDao.getEmail());
        validateDao.setResetToken(token);
        validateDao.setType("passwordReset");
        validateDao.setGmtCreate(new Date());
        validateDao.setGmtModified(new Date());
        return validateDaoMapper.insert(validateDao);
    }
    /**
     * pm_validate表中,通过token查找重置申请记录
     * @param token
     * @return
     */
    public ValidateDao findUserByResetToken(String token){
        return validateDaoMapper.selectByToken(token);
    }
    /**
     * 验证是否发送重置邮件:每个email的重置密码每日请求上限为requestPerDay次,与上一次的请求时间间隔为interval分钟。
     * @param email
     * @param requestPerDay
     * @param interval
     * @return
     */
    public boolean sendValidateLimitation(String email, long requestPerDay, long interval){
        List<ValidateDao> validateDaoList = validateDaoMapper.selectByEmail(email);
        // 若查无记录,意味着第一次申请,直接放行
        if (validateDaoList.isEmpty()) {
            return true;
        }
        // 有记录,则判定是否频繁申请以及是否达到日均请求上线
        long countTodayValidation = validateDaoList.stream().filter(x-> DateUtils.isSameDay(x.getGmtModified(), new Date())).count();
        Optional validate = validateDaoList.stream().map(ValidateDao::getGmtModified).max(Date::compareTo);
        Date dateOfLastRequest = new Date();
        if (validate.isPresent()) dateOfLastRequest = (Date) validate.get();
        long intervalForLastRequest = new Date().getTime() - dateOfLastRequest.getTime();
        return countTodayValidation <= requestPerDay && intervalForLastRequest >= interval * 60 * 1000;
    }
    /**
     * 验证连接是否失效:链接有两种情况失效 1.超时 2.最近请求的一次链接自动覆盖之前的链接(待看代码)
     * @param email
     * @param requestPerDay
     * @param interval
     * @return
     */
    public boolean validateLimitation(String email, long requestPerDay, long interval, String token){
        List<ValidateDao> validateDaoList = validateDaoMapper.selectByEmail(email);
        // 有记录才会调用该函数,只需判断是否超时
        Optional validate = validateDaoList.stream().map(ValidateDao::getGmtModified).max(Date::compareTo);
        Date dateOfLastRequest = new Date();
        if (validate.isPresent()) dateOfLastRequest = (Date) validate.get();
        long intervalForLastRequest = new Date().getTime() - dateOfLastRequest.getTime();
        Optional lastRequestToken = validateDaoList.stream().filter(x-> x.getResetToken().equals(token)).map(ValidateDao::getGmtModified).findAny();
        Date dateOfLastRequestToken = new Date();
        if (lastRequestToken.isPresent()) {
            dateOfLastRequestToken = (Date) lastRequestToken.get();
        }
        return intervalForLastRequest <= interval * 60 * 1000 && dateOfLastRequest == dateOfLastRequestToken;
    }
}

2.4.5编写 controller


@Api(tags = "通过邮件修改密码相关api")
@RestController
@RequestMapping(value = "/validate")
public class ValidateController {
    @Autowired
    private ValidateService validateService;
    @Autowired
    private UserService userService;
    //发送邮件的类
    @Autowired
    private JavaMailSender mailSender;
    //这里使用的是我们已经在配置问价中固定了的变量值,也就是通过这个邮箱向目标邮箱发送重置密码的邮件
    @Value("${spring.mail.username}")
    private String from;
    /**
     * 发送忘记密码邮件请求,每日申请次数不超过20次,每次申请间隔不低于1分钟
     * @param email
     * @param request
     * @return
     */
    @ApiOperation(value = "发送忘记密码邮件", notes = "发送忘记密码邮件")
    @RequestMapping(value = "/sendValidationEmail", method = {RequestMethod.POST})
    public RestResult sendValidationEmail(@ApiParam("邮箱地址") @RequestParam("email") String email,
                                                  HttpServletRequest request) throws MessagingException {
        RestResult restResult=new RestResult();
        UserDao userDao = userService.findUserByEmail(email);
        if (userDao == null){
            restResult.fail("该邮箱所属用户不存在");
        }else {
            if (validateService.sendValidateLimitation(email, 20,1)){
                // 若允许重置密码,则在pm_validate表中插入一行数据,带有token
                ValidateDao validateDao = new ValidateDao();
                validateService.insertNewResetRecord(validateDao, userDao, UUID.randomUUID().toString());
                // 设置邮件内容
                String appUrl = request.getScheme() + "://" + request.getServerName()+":"+request.getServerPort();
                MimeMessage mimeMessage = mailSender.createMimeMessage();
                // multipart模式
                MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true, "utf-8");
                mimeMessageHelper.setTo(email);
                mimeMessageHelper.setFrom(from);
                mimeMessageHelper.setSubject("重置密码");
                StringBuilder sb = new StringBuilder();
                sb.append("<html><head></head>");
                sb.append("<body><h1>点击下面的链接重置密码</h1>" +
                        "<a href = "+appUrl +"/validate/resetPassword?token="+validateDao.getResetToken()+">"+appUrl +"/validate/resetPassword?token=" +validateDao.getResetToken()+"</a></body>");
                sb.append("</html>");
                // 启用html
                mimeMessageHelper.setText(sb.toString(), true);
                validateService.sendPasswordResetEmail(mimeMessage);
//                SimpleMailMessage passwordResetEmail = new SimpleMailMessage();
//                passwordResetEmail.setFrom(from);
//                passwordResetEmail.setTo(email);
//                passwordResetEmail.setSubject("忘记密码");
//                passwordResetEmail.setText("您正在申请重置密码,请点击此链接重置密码: \n" +appUrl + "/validate/resetPassword?token=" + validateDao.getResetToken());
//                validateService.sendPasswordResetEmail(passwordResetEmail);
                Map<String,Object> map1=new HashMap<>();
                map1.put("token",validateDao.getResetToken());
                map1.put("link",appUrl +"/validate/resetPassword?token="+validateDao.getResetToken());
                map1.put("message","邮件已经发送");
                restResult.success(map1);
            }else {
                restResult.fail("操作过于频繁,请稍后再试!");
            }
        }
        return restResult;
    }
    /**
     * 将url的token和数据库里的token匹配,成功后便可修改密码,token有效期为5分钟
     * @param token
     * @param password
     * @param confirmPassword
     * @return
     */
    @ApiOperation(value = "重置密码,邮箱中的token有效时间为5分钟,每天每个用户最多发10次邮件", notes = "重置密码")
    @RequestMapping(value = "/resetPassword", method = RequestMethod.POST)
    public RestResult resetPassword(@ApiParam("token") @RequestParam("token") String token,
                                              @ApiParam("密码") @RequestParam("password") String password,
                                              @ApiParam("密码确认") @RequestParam("confirmPassword") String confirmPassword){
        RestResult restResult=new RestResult();
        // 通过token找到validate记录
        ValidateDao validateDao= validateService.findUserByResetToken(token);
        if (validateDao == null){
            restResult.fail("该重置请求不存在");
        }else {
            if (validateService.validateLimitation(validateDao.getEmail(), Long.MAX_VALUE, 5, token)){
                Integer userId = validateDao.getUserId();
                if (password.equals(confirmPassword)) {
                    UserDao userDao=new UserDao();
                    userDao.setPassword(password);
                    userDao.setUserId(userId);
                    userService.updateByPrimaryKeySelective(userDao);
                    restResult.success("成功重置密码");
                }else {
                    restResult.fail("确认密码和密码不一致,请重新输入");
                }
            }else {
                restResult.fail("该链接失效");
            }
        }
        return restResult;
    }
}


这里我自己在使用的过程中发现发送邮件的两种对象


SimpleMailMessage

这是最简单的邮件对象,它的邮件内容只能包含文字信息

下图是通过SimpleMailMessage 发送的邮件,无法显示成链接的形式


20201026170039323.png


MimeMessage

该对象可以往邮件里面写入HTML格式的数据,也就是能够包含链接,图片这些,不仅仅只好汉文字而已.


20201026170159369.png


3.效果演示


最后我们通过GIF来演示一下效果


20201026170938964.gif


整个过程主要就是邮件发送的过程比较慢,并且因为是前后端分离的项目,所以页面不是由我编写的,邮箱中的URL点击之后是不能直接使用的,大家的项目如果没有整合swagger的话,可以去看我一下我这几篇文章都有详细讲解如何配置swagger:


前后端接口测试神器Swagger基本使用

SSM整合Swagger

swagger跨域,404,弹窗问题解决

如果不想整合的话,可以在IDEA中下载RestfulToolk插件,我的这篇博客:RestfulToolk----IDEA在线接口测试插件神器,也有讲或者也可以去下载Postman这个接口测试工具,也能够很好的测试


必须要 前端解析完成后,定向到他们定义的页面上 ,并且也能够看到我已经将生成的token信息返回给了前端,所以前端只需要传入密码以及确认密码就行了.

这样通过邮件找回密码的功能就已经实现了


相关文章
|
6天前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
21 4
|
8天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
62 1
|
3天前
|
Java API 数据库
Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐
本文通过在线图书管理系统案例,详细介绍如何使用Spring Boot构建RESTful API。从项目基础环境搭建、实体类与数据访问层定义,到业务逻辑实现和控制器编写,逐步展示了Spring Boot的简洁配置和强大功能。最后,通过Postman测试API,并介绍了如何添加安全性和异常处理,确保API的稳定性和安全性。
8 0
|
17天前
|
Java 数据安全/隐私保护 Spring
springboot实现邮箱发送(激活码)功能
本文介绍了如何在Spring Boot应用中配置和使用邮箱发送功能,包括开启邮箱的SMTP服务、添加Spring Boot邮件发送依赖、配置application.properties文件,以及编写邮件发送的代码实现。
42 2
springboot实现邮箱发送(激活码)功能
|
17天前
|
安全 Java Linux
springboot实现黑名单和白名单功能
这篇文章介绍了如何在Spring Boot中实现黑名单和白名单功能,通过创建一个自定义的过滤器类并注册到Spring Boot应用中,以控制基于IP地址的访问权限。
31 1
springboot实现黑名单和白名单功能
|
9天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用
【10月更文挑战第8天】本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,通过 Spring Initializr 创建并配置 Spring Boot 项目,实现后端 API 和安全配置。接着,使用 Ant Design Pro Vue 脚手架创建前端项目,配置动态路由和菜单,并创建相应的页面组件。最后,通过具体实践心得,分享了版本兼容性、安全性、性能调优等注意事项,帮助读者快速搭建高效且易维护的应用框架。
18 3
|
10天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用
【10月更文挑战第7天】本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,通过 Spring Initializr 创建 Spring Boot 项目并配置 Spring Security。接着,实现后端 API 以提供菜单数据。在前端部分,使用 Ant Design Pro Vue 脚手架创建项目,并配置动态路由和菜单。最后,启动前后端服务,实现高效、美观且功能强大的应用框架。
13 2
|
10天前
|
NoSQL Java Redis
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
这篇文章介绍了如何使用Spring Boot整合Apache Shiro框架进行后端开发,包括认证和授权流程,并使用Redis存储Token以及MD5加密用户密码。
16 0
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
|
26天前
|
存储 前端开发 Java
Spring Boot 集成 MinIO 与 KKFile 实现文件预览功能
本文详细介绍如何在Spring Boot项目中集成MinIO对象存储系统与KKFileView文件预览工具,实现文件上传及在线预览功能。首先搭建MinIO服务器,并在Spring Boot中配置MinIO SDK进行文件管理;接着通过KKFileView提供文件预览服务,最终实现文档管理系统的高效文件处理能力。
117 11
|
22天前
|
缓存 NoSQL Java
Springboot自定义注解+aop实现redis自动清除缓存功能
通过上述步骤,我们不仅实现了一个高度灵活的缓存管理机制,还保证了代码的整洁与可维护性。自定义注解与AOP的结合,让缓存清除逻辑与业务逻辑分离,便于未来的扩展和修改。这种设计模式非常适合需要频繁更新缓存的应用场景,大大提高了开发效率和系统的响应速度。
42 2