Fms-Java版开发实录:11 密码重置功能 - 验证用户名和密码

Fms-Java版开发实录:11 密码重置功能 - 验证用户名和密码

密码重置功能自己从0开始写啦

在之前我自己开发的各种系统中,还从来没有开发过密码重置功能。这次我没有看任何的东西,纯粹凭自己现在已经掌握和了解的内容来编写密码重置的功能。

整体功能设计

这是我自己设计出来的密码重置功能的流程:
passwordResetDesign

然后我就要根据这个来写程序,这个一长串地址我准备使用UUID来生成,发送邮件的功能在之前生成Spring Boot项目的时候就勾选了Java Mail Sender,要来学习一下才行。

Java Mail Sender发送邮件

我在这里(这个站也是一个相当好的Spring教程站)找了个教程,跟着试验下来,基本搞定了。

Spring Boot邮件发送的设置

因为在项目中已经包含了如下配置:

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

所以就包含了发送邮件的功能。接下来就是要先配置一下发送邮件的SMTP服务:

spring.mail.host=smtp.vip.sina.com
spring.mail.port=25
spring.mail.username=lee0709@vip.sina.com
spring.mail.password=******
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.test-connection=true

配置依次是SMTP服务器,端口号,用户名,密码和SMTP需要验证,开启TLS,不过经过我试验,开不开启都能发送。
最后一行是配置项目在启动的时候是否去连接邮件服务器进行验证,如果连接不成功,项目就不会启动,这个目前就暂且打开。经过这么配置之后,重启项目,发现没有报错,就说明准备工作做好了。

发送邮件业务层编写

因为我想让用户直接打开链接,因此需要发送HTML格式的邮件。所以现在就要来看一下Java Mail Sender的简单使用。

老套路了,想干什么先得编写一个业务层的东西,我们就叫其EmailSendingService,别让控制器干太多事情。在Spring Boot中,自动提供了JavaMailSenderImpl,所以在业务类中直接自动装配即可:

package cc.conyli.fms.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

@Service
public class EmailSendingService {

    private final JavaMailSender emailSender;

    @Autowired
    public EmailSendingService(JavaMailSender emailSender) {
        this.emailSender = emailSender;
    }
}

然后就是编写如何使用EmailSender来发送邮件。先编写发送纯文本邮件的功能,对应的是SimpleMailMessage对象,可以为其设置发送对象,主题和内容,如下:

@Transactional
public boolean sendSimpleMessage(String to, String subject, String message) {
    try {
        SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
        simpleMailMessage.setFrom("lee0709@vip.sina.com");
        simpleMailMessage.setTo(to);
        simpleMailMessage.setSubject(subject);
        simpleMailMessage.setText(message);
        emailSender.send(simpleMailMessage);
        return true; 
    } catch (Exception e) {
        return false;
    }
}

发送HTML邮件对应的是MimeMessage类,用法与SimpleMailMessage略有不同,之后再来完善:

@Transactional
public boolean sendMimeMessage(String to, String subject, String message) {
    MimeMessage mimeMessage = emailSender.createMimeMessage();
    try {
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
        helper.setTo(to);
        helper.setSubject(subject);
        helper.setText(message, true);
        emailSender.send(mimeMessage);
        return true;
    } catch (Exception e) {
        return false;
    }
}

这两个方法在完成发送之后都会返回true,在失败后会返回false,控制器可以借此来判断是否发送成功,从而返回对应的页面。这两个方法后期还需要完善,但是基本功能已经具备,继续下一步工作。

邮件功能相关属性的设置

在下一步开发之前,我想配置给用户发送邮件时的邮件来源,网页前缀以及链接过期的时间。于是我在application.properties中添加了几行配置:

password.mailFrom=lee0709@vip.sina.com
password.path=http://localhost:8080/account/reset/
password.invalidInterval=6

这几个属性用来设置常用的邮件发送人,拼接的链接地址,以及链接过期的时间(小时)。我不想用自定义属性文件,就使用application.properties一个就足够。在configure包里创建一个EmailProperties类,然后使用@Value属性和SpringEL表达式来加载:

package cc.conyli.fms.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class EmailProperties {
    @Value("${password.mailFrom}")
    private String from;

    @Value("${password.path}")
    private String path;

    @Value("${password.invalidInterval}")
    private String invalidInterval;

    public String getFrom() {
        return from;
    }

    public String getPath() {
        return path;
    }

    public String getInvalidInterval() {
        return invalidInterval;
    }
}

这样就可以用这几个属性来控制发送邮件的路径以及验证码。

校验用户名和密码

页面分两个,一种是用户从从登录页面选择密码重置后进入的页面,对应的控制器是/account/reset。还有一个是用户直接访问的特定地址,接在/account/reset/{path}之后,用于返回用户设置密码的页面。
这里我们先来编写校验用户名和密码的页面及背后的逻辑。

密码重置页面

先来编写页面1,也就是用户从登录页面选择密码重置后进入的页面,对应的控制器是/account/reset,这个路径是完全放开的,需要输入用户名和对应的电子邮箱:

<!doctype html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org"
      th:replace="~{/shared/standard::layout(~{::title}, ~{::style}, ~{::main}, ~{::script})}">
<head>
    <title>重置密码</title>
    <style>
        ......
    </style>
</head>

<body>
<main class="form-signin">
    <form action="/account/reset" th:action="@{/account/reset}" method="post">
        <img class="mb-4" src="/img/sinochem.png" th:src="@{/img/sinochem.png}" alt="中化集团">
        <h1 class="h3 mb-3 fw-normal">重置密码</h1>
        <div th:if="${failed} != null" class="alert alert-danger" role="alert">
            用户名与电子邮件不匹配
        </div>
        <div class="form-floating">
            <input type="text" class="form-control" id="floatingInput" placeholder="用户名" name="username" required>
            <label for="floatingInput">用户名</label>
        </div>
        <div class="form-floating">
            <input type="email" name="email" class="form-control" id="floatingEmail" placeholder="电子邮件" required>
            <label for="floatingEmail">电子邮件</label>
        </div>
        <p class="mt-3 mb-3">返回<a href="/account/login" th:href="@{/account/login}"
                                       style="text-decoration: none">登录</a>或<a href="mailto:liyiming@sinochem.com"
                                                                               style="text-decoration: none">联系管理员</a></p>
        <button class="w-100 btn btn-lg btn-primary" type="submit">提交</button>
        <p class="mt-3 mb-3 text-muted">Copyrights &copy;2021
            <a href="https://conyli.cc" target="_blank" style="text-decoration: none">https://conyli.cc</a>
        </p>
        <p class="mt-3 mb-3 text-muted">All Rights Reserved</p>
    </form>
</main>
<script src=""></script>
</body>
</html>

这个页面的核心就是把用户名和密码POST/account/reset目录,检查两者是否相符,如果相符就提示发送邮件并让用户在邮件中访问链接,不相符就提示错误信息。

业务层验证用户名和电子邮件相符

这里的逻辑是获取用户名和电子邮件,然后到业务层做一个校验,看二者是否相符。相符就发送电子邮件,不相符向页面返回错误信息。

这里逻辑可以简单一些,不用再靠VO验证,直接获取POST过来的数据即可,前端反正加上required等参数,能验证一点是一点。

写到这里我突然想到,其实之前的处理用户修改email和密码的逻辑,在控制器里写了很多业务逻辑,应该都写在业务层才对。这里我给UserService添加一个检验用户名和密码是否匹配的方法:

@Transactional
public boolean validateUsernameAndEmail(String username, String email) {
    if (!userDao.existsByUsername(username)) {
        return false;
    } else {
        return userDao.findByUsername(username).getUserInfo().getEmail().equals(email);
    }
}

通用info页面

在编写控制器之前,想到之前的passwordChanged.html,这个页面用来展示密码修改完成后的提示,我想到可以把这个页面改成info.html,然后将要展示的内容传入即可,密码重置之后也可以用这个页面来进行提示:

<!doctype html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org"
      th:replace="~{/shared/standard::layout(~{::title}, ~{::style}, ~{::main}, ~{::script})}">
<head>
    <title th:text="${customTitle}">请重新登录</title>
    <style>
       ......
    </style>
</head>
<main class="form-signin">
    <form action="/account/signout" th:action="@{/account/signout}" method="post">
        <h4 th:text="${info}" class="mb-3">请重新登录</h4>
        <a class="w-100 btn btn-lg btn-primary" th:href="@{/account/login}">登录</a>
    </form>
</main>
<body>
<main>

</main>
<script src=""></script>
</body>
</html>

目前使用到info.html的控制器只有修改密码成功后的控制器,略微修改一下:

request.getSession().invalidate();
model.addAttribute("customTitle", "返回登录");
model.addAttribute("info", "密码已修改,请返回登录");
return "/account/info";

这里要注意的是,传入的title变量名不要使用title,否则会导致模板渲染不正确,所以改成了customTitle

验证用户名和电子邮件的控制器

控制器的GET请求比较简单,返回页面即可,POST请求目前还无法全部编写,但可以把基础框架撘起来。

@GetMapping("/reset")
public String passwordResetPage() {
    return "/account/reset";
}

@PostMapping("/reset")
public String validateReset(
        @RequestParam("username") String username,
        @RequestParam("email") String email,
        Model model
) {
    if (userService.validateUsernameAndEmail(username, email)) {

        //        待编写

        model.addAttribute("customTitle", "返回登录");
        model.addAttribute("info", "邮件已发送,请按照邮件重置密码");
        return "/account/info";
    } else {
        model.addAttribute("failed", "failed");
        return "/account/reset";
    }
}

这个控制器逻辑很简单,靠业务层来验证用户名和密码,如果验证失败,就返回错误页面。如果验证成功,就要继续进行如下逻辑:

  1. 生成一条长字符串并和其他数据一起保存入数据库
  2. 发送电子邮件

当然这只是最宏观的角度,为了编写这两条逻辑,还要做很多准备工作,且听下文分解。

LICENSED UNDER CC BY-NC-SA 4.0
Comment