【Springboot】发送邮件、重置密码业务实战

前言

忘记密码并通过邮件重置密码是一个常见的业务需求,在开发我的个人小项目过程中,也需要用到这个业务,今天就给大家带来一个业务实战。

开发环境

  • springboot:1.5.16.RELEASE

业务流程

根据controller中函数分为两个部分:

  1. 用户申请重置邮件:
  • 用户在页面中输入邮箱
  • 服务器检查是否允许重置(邮箱所指向用户是否存在,重置是否过于频繁,重置是否到达日请求上限)
  • 验证通过后,想validate表写入申请记录,包含token,用户邮箱和id
  • 发送邮件(包含带有token的链接)
  • 用户点击邮件内连接
  • 跳转到新密码输入网页
  • 提交重置密码请求(POST中包含token,新密码)
  1. 用户重置密码
  • 服务器验证token(token是否过期,该用户是否发起过其它新token)
  • 通过validate表记录查找用户id,修改用户密码

实战

  1. pom.xml添加email依赖
1
2
3
4
5
<!--邮件: email-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
  1. 添加pm_validate表结构

其中reset_token由UUID生成,type默认为resetPassword(方便以后新增需求),user_id为用户表用户id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- ----------------------------
-- Table structure for 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;

生成或编写对应pojo和mapper。,由于我使用了mybatis-generator插件,需要运行插件生成对应pojo和mapper。

  1. 修改application.properties,添加邮箱配置
1
2
3
4
5
6
7
# 发送邮件配置
spring.mail.host=smtp.gmail.com
spring.mail.username=xxxxxx@gmail.com
spring.mail.password=xxxxxxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
  1. 编写controller和service
  • ValidateController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@RestController
@RequestMapping(value = "/validate")
public class ValidateController {

@Autowired
private ValidateService validateService;

@Autowired
private UserService userService;

@Value("${spring.mail.username}")
private String from;

/**
* 发送忘记密码邮件请求,每日申请次数不超过5次,每次申请间隔不低于1分钟
* @param email
* @param request
* @return
*/
@ApiOperation(value = "发送忘记密码邮件", notes = "发送忘记密码邮件")
@RequestMapping(value = "/sendValidationEmail", method = {RequestMethod.POST})
public ResponseData<String> sendValidationEmail(@ApiParam("邮箱地址") @RequestParam("email") String email,
HttpServletRequest request){
ResponseData<String> responseData = new ResponseData<>();
List<User> users = userService.findUserByEmail(email);
if (users == null){
responseData.jsonFill(2, "该邮箱所属用户不存在", null);
}else {
if (validateService.sendValidateLimitation(email, 5,1)){
// 若允许重置密码,则在pm_validate表中插入一行数据,带有token
Validate validate = new Validate();
validateService.insertNewResetRecord(validate, users.get(0), UUID.randomUUID().toString());
// 设置邮件内容
String appUrl = request.getScheme() + "://" + request.getServerName();
SimpleMailMessage passwordResetEmail = new SimpleMailMessage();
passwordResetEmail.setFrom(from);
passwordResetEmail.setTo(email);
passwordResetEmail.setSubject("【电商价格监控】忘记密码");
passwordResetEmail.setText("您正在申请重置密码,请点击此链接重置密码: \n" + appUrl + "/validate/reset?token=" + validate.getResetToken());
validateService.sendPasswordResetEmail(passwordResetEmail);
responseData.jsonFill(1, null, null);
}else {
responseData.jsonFill(2,"操作过于频繁,请稍后再试!",null);
}
}
return responseData;
}

/**
* 将url的token和数据库里的token匹配,成功后便可修改密码,token有效期为60分钟
* @param token
* @param password
* @param confirmPassword
* @return
*/
@ApiOperation(value = "重置密码", notes = "重置密码")
@RequestMapping(value = "/resetPassword", method = RequestMethod.POST)
public ResponseData<String> resetPassword(@ApiParam("token") @RequestParam("token") String token,
@ApiParam("密码") @RequestParam("password") String password,
@ApiParam("密码确认") @RequestParam("confirmPassword") String confirmPassword){
ResponseData<String> responseData = new ResponseData<>();
// 通过token找到validate记录
List<Validate> validates = validateService.findUserByResetToken(token);
if (validates == null){
responseData.jsonFill(2,"该重置请求不存在",null);
}else {
Validate validate = validates.get(0);
if (validateService.validateLimitation(validate.getEmail(), Long.MAX_VALUE, 60, token)){
Integer userId = validate.getUserId();
if (password.equals(confirmPassword)) {
userService.updatePassword(password, userId);
responseData.jsonFill(1, null,null);
}else {
responseData.jsonFill(2,"确认密码和密码不一致,请重新输入", null);
}
}else {
responseData.jsonFill(2,"该链接失效",null);
}
}
return responseData;
}
}
  • ValidateService
1
2
3
4
5
6
7
public interface ValidateService {
void sendPasswordResetEmail(SimpleMailMessage email);
int insertNewResetRecord(Validate validate, User users, String token);
List<Validate> findUserByResetToken(String resetToken);
boolean validateLimitation(String email, long requestPerDay, long interval, String token);
boolean sendValidateLimitation(String email, long requestPerDay, long interval);
}
  • ValidateServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
@Service
public class ValidateServiceImpl implements ValidateService {

@Autowired
private JavaMailSender javaMailSender;

@Autowired
private ValidateMapper validateMapper;

/**
* 发送邮件:@Async进行异步调用发送邮件接口
* @param email
*/
@Override
@Async
public void sendPasswordResetEmail(SimpleMailMessage email){
javaMailSender.send(email);
}

/**
* 在pm_validate表中插入一条validate记录,userid,email属性来自pm_user表,token由UUID生成
* @param validate
* @param users
* @param token
* @return
*/
@Override
public int insertNewResetRecord(Validate validate, User users, String token){
validate.setUserId(users.getId());
validate.setEmail(users.getEmail());
validate.setResetToken(token);
validate.setType("passwordReset");
validate.setGmtCreate(new Date());
validate.setGmtModified(new Date());
return validateMapper.insert(validate);
}

/**
* pm_validate表中,通过token查找重置申请记录
* @param token
* @return
*/
@Override
public List<Validate> findUserByResetToken(String token){
ValidateExample validateExample = new ValidateExample();
ValidateExample.Criteria criteria = validateExample.createCriteria();
criteria.andResetTokenEqualTo(token);
return validateMapper.selectByExample(validateExample);
}

/**
* 验证是否发送重置邮件:每个email的重置密码每日请求上限为requestPerDay次,与上一次的请求时间间隔为interval分钟。
* @param email
* @param requestPerDay
* @param interval
* @return
*/
@Override
public boolean sendValidateLimitation(String email, long requestPerDay, long interval){
ValidateExample validateExample = new ValidateExample();
ValidateExample.Criteria criteria= validateExample.createCriteria();
criteria.andEmailEqualTo(email);
List<Validate> validates = validateMapper.selectByExample(validateExample);
// 若查无记录,意味着第一次申请,直接放行
if (validates.isEmpty()) {
return true;
}
// 有记录,则判定是否频繁申请以及是否达到日均请求上线
long countTodayValidation = validates.stream().filter(x->DateUtils.isSameDay(x.getGmtModified(), new Date())).count();
Optional validate = validates.stream().map(Validate::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
*/
@Override
public boolean validateLimitation(String email, long requestPerDay, long interval, String token){
ValidateExample validateExample = new ValidateExample();
ValidateExample.Criteria criteria= validateExample.createCriteria();
criteria.andEmailEqualTo(email);
List<Validate> validates = validateMapper.selectByExample(validateExample);
// 有记录才会调用该函数,只需判断是否超时
Optional validate = validates.stream().map(Validate::getGmtModified).max(Date::compareTo);
Date dateOfLastRequest = new Date();
if (validate.isPresent()) dateOfLastRequest = (Date) validate.get();
long intervalForLastRequest = new Date().getTime() - dateOfLastRequest.getTime();

Optional lastRequestToken = validates.stream().filter(x-> x.getResetToken().equals(token)).map(Validate::getGmtModified).findAny();
Date dateOfLastRequestToken = new Date();
if (lastRequestToken.isPresent()) {
dateOfLastRequestToken = (Date) lastRequestToken.get();
}
return intervalForLastRequest <= interval * 60 * 1000 && dateOfLastRequest == dateOfLastRequestToken;
}
}

结语

如上实现了整个重置密码流程,前端网页自行设计实现。

关注我

我是蛮三刀把刀,目前为后台开发工程师。主要关注后台开发,网络安全,Python爬虫等技术。

来微信和我聊聊:yangzd1102

Github:https://github.com/qqxx6661

原创博客主要内容

  • 笔试面试复习知识点手册
  • Leetcode算法题解析(前150题)
  • 剑指offer算法题解析
  • Python爬虫相关技术分析和实战
  • 后台开发相关技术分析和实战

同步更新以下博客

1. Csdn

http://blog.csdn.net/qqxx6661

拥有专栏:Leetcode题解(Java/Python)、Python爬虫开发、面试助攻手册

2. 知乎

https://www.zhihu.com/people/yang-zhen-dong-1/

拥有专栏:码农面试助攻手册

3. 掘金

https://juejin.im/user/5b48015ce51d45191462ba55

4. 简书

https://www.jianshu.com/u/b5f225ca2376

个人公众号:Rude3Knife

个人公众号:Rude3Knife

如果文章对你有帮助,不妨收藏起来并转发给您的朋友们~

个人项目:电商价格监控网站

本人长期维护的个人项目,完全免费,请大家多多支持。

实现功能

  • 京东商品监控:设置商品ID和预期价格,当商品价格【低于】设定的预期价格后自动发送邮件提醒用户。(一小时以内)
  • 京东品类商品监控:用户订阅特定品类后,该类降价幅度大于7折的【自营商品】会被选出并发送邮件提醒用户。
  • 品类商品浏览,商品历史价格曲线,商品历史最高最低价
  • 持续更新中…

网站地址

https://pricemonitor.online/