美文网首页SpringBoot实战系列
[Springboot]发送邮件、重置密码业务实战

[Springboot]发送邮件、重置密码业务实战

作者: 蛮三刀酱 | 来源:发表于2019-02-15 13:14 被阅读0次

    前言

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

    开发环境

    • springboot:1.5.16.RELEASE

    业务流程

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

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

    实战

    1. pom.xml添加email依赖
    <!--邮件: 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

    -- ----------------------------
    -- 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,添加邮箱配置
    # 发送邮件配置
    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
    @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
    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;
    
        /**
         * 发送邮件:@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;
        }
    }
    

    结语

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

    相关文章

      网友评论

        本文标题:[Springboot]发送邮件、重置密码业务实战

        本文链接:https://www.haomeiwen.com/subject/fwmpeqtx.html