前言
忘記密碼并通過郵件重置密碼是一個常見的業(yè)務(wù)需求胳徽,在開發(fā)我的個人小項(xiàng)目過程中,也需要用到這個業(yè)務(wù)爽彤,今天就給大家?guī)硪粋€業(yè)務(wù)實(shí)戰(zhàn)养盗。
開發(fā)環(huán)境
- springboot:1.5.16.RELEASE
業(yè)務(wù)流程
根據(jù)controller中函數(shù)分為兩個部分:
- 用戶申請重置郵件:
- 用戶在頁面中輸入郵箱
- 服務(wù)器檢查是否允許重置(郵箱所指向用戶是否存在,重置是否過于頻繁适篙,重置是否到達(dá)日請求上限)
- 驗(yàn)證通過后往核,想validate表寫入申請記錄,包含token匙瘪,用戶郵箱和id
- 發(fā)送郵件(包含帶有token的鏈接)
- 用戶點(diǎn)擊郵件內(nèi)連接
- 跳轉(zhuǎn)到新密碼輸入網(wǎng)頁
- 提交重置密碼請求(POST中包含token铆铆,新密碼)
- 用戶重置密碼
- 服務(wù)器驗(yàn)證token(token是否過期,該用戶是否發(fā)起過其它新token)
- 通過validate表記錄查找用戶id丹喻,修改用戶密碼
實(shí)戰(zhàn)
- pom.xml添加email依賴
<!--郵件: email-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
- 添加pm_validate表結(jié)構(gòu)
其中reset_token由UUID生成薄货,type默認(rèn)為resetPassword(方便以后新增需求),user_id為用戶表用戶id
-- ----------------------------
-- 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;
生成或編寫對應(yīng)pojo和mapper碍论。谅猾,由于我使用了mybatis-generator插件,需要運(yùn)行插件生成對應(yīng)pojo和mapper鳍悠。
- 修改application.properties税娜,添加郵箱配置
# 發(fā)送郵件配置
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
- 編寫controller和service
- ValidateController
@RestController
@RequestMapping(value = "/validate")
public class ValidateController {
@Autowired
private ValidateService validateService;
@Autowired
private UserService userService;
@Value("${spring.mail.username}")
private String from;
/**
* 發(fā)送忘記密碼郵件請求,每日申請次數(shù)不超過5次藏研,每次申請間隔不低于1分鐘
* @param email
* @param request
* @return
*/
@ApiOperation(value = "發(fā)送忘記密碼郵件", notes = "發(fā)送忘記密碼郵件")
@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表中插入一行數(shù)據(jù),帶有token
Validate validate = new Validate();
validateService.insertNewResetRecord(validate, users.get(0), UUID.randomUUID().toString());
// 設(shè)置郵件內(nèi)容
String appUrl = request.getScheme() + "://" + request.getServerName();
SimpleMailMessage passwordResetEmail = new SimpleMailMessage();
passwordResetEmail.setFrom(from);
passwordResetEmail.setTo(email);
passwordResetEmail.setSubject("【電商價格監(jiān)控】忘記密碼");
passwordResetEmail.setText("您正在申請重置密碼蠢挡,請點(diǎn)擊此鏈接重置密碼: \n" + appUrl + "/validate/reset?token=" + validate.getResetToken());
validateService.sendPasswordResetEmail(passwordResetEmail);
responseData.jsonFill(1, null, null);
}else {
responseData.jsonFill(2,"操作過于頻繁弧岳,請稍后再試凳忙!",null);
}
}
return responseData;
}
/**
* 將url的token和數(shù)據(jù)庫里的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("密碼確認(rèn)") @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,"確認(rèn)密碼和密碼不一致涧卵,請重新輸入", null);
}
}else {
responseData.jsonFill(2,"該鏈接失效",null);
}
}
return responseData;
}
}
- ValidateService
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
@Service
public class ValidateServiceImpl implements ValidateService {
@Autowired
private JavaMailSender javaMailSender;
@Autowired
private ValidateMapper validateMapper;
/**
* 發(fā)送郵件:@Async進(jìn)行異步調(diào)用發(fā)送郵件接口
* @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);
}
/**
* 驗(yàn)證是否發(fā)送重置郵件:每個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;
}
// 有記錄伤提,則判定是否頻繁申請以及是否達(dá)到日均請求上線
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;
}
/**
* 驗(yàn)證連接是否失效:鏈接有兩種情況失效 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);
// 有記錄才會調(diào)用該函數(shù),只需判斷是否超時
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;
}
}
結(jié)語
如上實(shí)現(xiàn)了整個重置密碼流程认烁,前端網(wǎng)頁自行設(shè)計(jì)實(shí)現(xiàn)肿男。