1.前言
之前在大学里面做项目的时候碰到修改密码那一块的,自己当时都是做的很简单的逻辑,也想过怎么通过邮箱或者手机号这种进一步验证身份来修改密码,但是自己当时太菜了,也没怎么好好钻研,所以就一直没尝试过那样的功能,但是这次公司项目里面可能会用到,于是自己找了找教程看了看,发现实现起来不难,毕竟别人已经把轮子已经造好了,但是其中还是遇到了一些问题,还是费了不少时间.希望这篇教程能过对你有所帮助.
2.步骤
2.1导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
导入这个依赖就够了
2.2开启邮箱的SMTP服务
这里我以 QQ邮箱 举例,其他的邮箱可以自行查看教程
这里开启比较简单中途就是需要我们发送短信去验证我们的身份
之后我们还需要去开启我们的授权码
这个过程也需要我们发送短信验证我们的身份,验证之后就会生成我们的授权码
可以看到下面的小子,授权码可以有多个,如果忘记了,我们可以重新过来验证身份生成授权码,所以不用担心记不住授权码.
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 编写逻辑的步骤
在编写逻辑之前我们先来梳理一下我们通过邮箱找回密码的整体流程
这是简化的逻辑,现在我们将它进行细化
主要思想如下图所示:
了解了整个流程之后,我们来编写
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 发送的邮件,无法显示成链接的形式
MimeMessage
该对象可以往邮件里面写入HTML格式的数据,也就是能够包含链接,图片这些,不仅仅只好汉文字而已.
3.效果演示
最后我们通过GIF来演示一下效果
整个过程主要就是邮件发送的过程比较慢,并且因为是前后端分离的项目,所以页面不是由我编写的,邮箱中的URL点击之后是不能直接使用的,大家的项目如果没有整合swagger的话,可以去看我一下我这几篇文章都有详细讲解如何配置swagger:
前后端接口测试神器Swagger基本使用
SSM整合Swagger
swagger跨域,404,弹窗问题解决
如果不想整合的话,可以在IDEA中下载RestfulToolk插件,我的这篇博客:RestfulToolk----IDEA在线接口测试插件神器,也有讲或者也可以去下载Postman这个接口测试工具,也能够很好的测试
必须要 前端解析完成后,定向到他们定义的页面上 ,并且也能够看到我已经将生成的token信息返回给了前端,所以前端只需要传入密码以及确认密码就行了.
这样通过邮件找回密码的功能就已经实现了