Fms-Java版开发实录:16 管理员修改用户密码及邮件服务器配置

Fms-Java版开发实录:16 管理员修改用户密码及邮件服务器配置

尽量将配置都搬到数据库中,这次是让管理员可以修改邮件发送设置

在目前的系统中,尚未初始化管理员的时候,是禁止用户注册的。所以不存在用户能够重置密码的情况,因为系统中还没没有用户。一旦完成了初始化管理员,假如管理员忘记了密码,就可以通过邮件重置。不过目前的邮件服务器我写死在了配置文件中,其实应该让用户输入自己的邮件服务器地址或者采用默认,这样才行。我打算把邮件相关的配置,也放入数据库中,然后让管理员来进行配置。发送邮件失败反正都是联系管理员,所以还要给管理员一个设置用户密码的界面,这个加在修改用户信息的部分就可以。

给管理员添加修改用户密码的功能

这个很简单,只需要多一个输入框即可,不过要提示别有事没事去更改其他用户的密码。页面上添加一个输入框如下:

<h5 class="mt-4">修改密码</h5>
<div class="col-sm-4 col-6">
    <label for="password" class="form-label h5">
    <input type="password" class="form-control" id="password" name="password">
    </label>
</div>
<p class="text-secondary">推荐由用户自行重置密码</p>

控制器稍作修改,根据password去掉空白之后的长度决定是否修改密码:

@PostMapping("/user")
public String updateUser(@RequestParam(name = "id") int id,
                         @RequestParam(name = "role") String roleName,
                         @RequestParam(name = "enabled", required = false) String enabled,
                         @RequestParam(name = "password", required = false) String password

) {
    User user = userService.getUserById(id);
    //设置密码
    if (password.trim().length() > 0) {
        user.setPassword(passwordEncoder.encode(password));
    }
    List<Role> roles = new ArrayList<>();
    roles.add(roleDao.findByRole(roleName));
    user.setRoles(roles);
    user.setEnabled(enabled != null);

    //强制让用户登录失效
    userService.saveAndRemoveSession(user);
    return "redirect:/admin/user";
}

重新编写邮件服务逻辑

目前的邮件服务器的配置,我写在了application.properties中,然后还使用了一个属性类将其取出。现在我打算也将其写入到option表中,在初始化的时候写入我的邮件服务器地址,然后让管理员来自行修改。在发送邮件的时候从数据库中取出对应的属性来发送邮件。

项目启动时写入邮件发送地址

邮件服务器需要如下几个属性:

  1. 用户名,也就是电子邮件地址
  2. 密码
  3. SMTP服务器的地址
  4. 端口
  5. 编码
  6. SMTP服务器是否需要验证,一般都会需要

那么就在项目初始化的时候将其写入数据库,在dataInitialize.sql中添加:

-- 写入邮件服务器默认配置
INSERT INTO  options(create_time, option_key, option_value, update_time)
VALUES (CURRENT_TIMESTAMP, 'emailInitialized', 'false', CURRENT_TIMESTAMP)
ON CONFLICT (option_key) DO NOTHING;

INSERT INTO  options(create_time, option_key, option_value, update_time)
VALUES (CURRENT_TIMESTAMP, 'emailUsername', 'lee0709@vip.sina.com', CURRENT_TIMESTAMP)
ON CONFLICT (option_key) DO NOTHING;

INSERT INTO  options(create_time, option_key, option_value, update_time)
VALUES (CURRENT_TIMESTAMP, 'emailPassword', 'jenny0217!@', CURRENT_TIMESTAMP)
ON CONFLICT (option_key) DO NOTHING;

INSERT INTO  options(create_time, option_key, option_value, update_time)
VALUES (CURRENT_TIMESTAMP, 'emailHost', 'smtp.vip.sina.com', CURRENT_TIMESTAMP)
ON CONFLICT (option_key) DO NOTHING;

INSERT INTO  options(create_time, option_key, option_value, update_time)
VALUES (CURRENT_TIMESTAMP, 'emailPort', '25', CURRENT_TIMESTAMP)
ON CONFLICT (option_key) DO NOTHING;

INSERT INTO  options(create_time, option_key, option_value, update_time)
VALUES (CURRENT_TIMESTAMP, 'emailEncoding', 'UTF-8', CURRENT_TIMESTAMP)
ON CONFLICT (option_key) DO NOTHING;

INSERT INTO  options(create_time, option_key, option_value, update_time)
VALUES (CURRENT_TIMESTAMP, 'emailAuth', 'true', CURRENT_TIMESTAMP)
ON CONFLICT (option_key) DO NOTHING;

创建控制器需要的VO

编写好之后,Option这个PO已经有了,我们要创建一个VO来对应页面上用户的设置,这个也比较简单:

public class EmailSetting {

    @NotEmpty(message = "邮件地址不能为空")
    @Email(message = "邮件地址不正确")
    @Length(max = 255, message = "长度不能超过255个字符")
    private String emailUsername;

    @NotEmpty(message = "密码不能为空")
    @Length(max = 255, message = "长度不能超过255个字符")
    private String emailPassword;

    @NotEmpty(message = "服务器地址不能为空")
    @Length(max = 255, message = "长度不能超过255个字符")
    private String emailHost;

    @Digits(fraction = 0, integer = 5, message = "端口号应该为5位整数")
    private int emailPort;

    @NotEmpty(message = "编码类型不能为空")
    @Length(max = 255, message = "长度不能超过255个字符")
    private String emailEncoding;

    private boolean emailAuth;
}

这里要注意的是@NotEmpty注解不能用于int类型上,而布尔型值由于有默认值,根本不需要去加验证。

编写页面

这个也是套路了,一次性把验证错误的样式也编写好,代码多一点,但比较简单:

<main class="container">
    <h2 class="pb-2 py-3">邮件服务器设置</h2>
    <p class="text-secondary" th:if="${!emailInitialized}">当前正在使用开发者的邮件服务器。</p>
    <p class="text-secondary" th:if="${!emailInitialized}">如果您不了解邮件服务器,请不要修改以下设置。</p>

    <div class="row">
        <div class="col-12 col-sm-8 col-md-6 col-lg-4">
            <div th:if="${failed}" class="alert alert-danger" role="alert">
                测试邮件发送失败,请重新设置
            </div>
            <div th:if="${failed!=null and !failed}" class="alert alert-success" role="alert">
                已更新邮件服务器设置
            </div>
            <form action="/admin/email" th:action="@{/admin/email}" th:object="${emailSetting}" method="post">
                <div class="input-group mb-3">
                    <span class="input-group-text" id="username">邮件地址</span>
                    <input th:classappend="${#fields.hasErrors('emailUsername')}? 'is-invalid'" type="text"
                           class="form-control" th:field="*{emailUsername}" th:placeholder="*{emailUsername}"
                           aria-label="username" aria-describedby="emailUsernameError" required>
                    <div id="emailUsernameError" th:if="${#fields.hasErrors('emailUsername')}"
                         th:errors="*{emailUsername}"
                         class="invalid-feedback"></div>
                </div>

                <div class="input-group mb-3">
                    <span class="input-group-text" id="password">登录密码</span>
                    <input th:classappend="${#fields.hasErrors('emailPassword')}? 'is-invalid'" type="password"
                           class="form-control" th:field="*{emailPassword}" aria-label="password"
                           aria-describedby="emailPasswordError" required>
                    <div id="emailPasswordError" th:if="${#fields.hasErrors('emailPassword')}"
                         th:errors="*{emailPassword}"
                         class="invalid-feedback"></div>
                </div>
                <div class="input-group mb-3">
                    <span class="input-group-text" id="host">服务器地址</span>
                    <input th:classappend="${#fields.hasErrors('emailHost')}? 'is-invalid'"
                           type="text" class="form-control" th:field="*{emailHost}" th:placeholder="*{emailHost}"
                           aria-label="host"
                           aria-describedby="emailHostError" required>
                    <div id="emailHostError" th:if="${#fields.hasErrors('emailHost')}"
                         th:errors="*{emailHost}"
                         class="invalid-feedback"></div>
                </div>
                <div class="input-group mb-3">
                    <span class="input-group-text" id="port">端口号</span>
                    <input th:classappend="${#fields.hasErrors('emailPort')}? 'is-invalid'" type="number" step="1"
                           class="form-control" th:field="*{emailPort}"
                           th:placeholder="*{emailPort}" aria-label="port"
                           aria-describedby="emailPortError" required>
                    <div id="emailPortError" th:if="${#fields.hasErrors('emailPort')}"
                         th:errors="*{emailPort}"
                         class="invalid-feedback"></div>
                </div>
                <div class="input-group mb-3">
                    <span class="input-group-text" id="encoding">邮件编码</span>
                    <input th:classappend="${#fields.hasErrors('emailEncoding')}? 'is-invalid'" type="text"
                           class="form-control" th:field="*{emailEncoding}"
                           th:placeholder="*{emailEncoding}" aria-label="encoding"
                           aria-describedby="emailEncodingError" required>
                    <div id="emailEncodingError" th:if="${#fields.hasErrors('emailEncoding')}"
                         th:errors="*{emailEncoding}"
                         class="invalid-feedback"></div>
                </div>
                <span>SMTP服务器需要验证:</span>
                <div class="form-check form-check-inline">
                    <input class="form-check-input" type="radio" name="emailAuth" id="superuser" value="true"
                           th:checked="${emailSetting.emailAuth}">
                    <label class="form-check-label" for="superuser">是</label>
                </div>
                <div class="form-check form-check-inline">
                    <input class="form-check-input" type="radio" name="emailAuth" id="user" value="false"
                           th:checked="${!emailSetting.emailAuth}">
                    <label class="form-check-label" for="user">否</label>
                </div>
                <div class="mt-3">
                    <button type="submit" class="btn btn-primary">提交</button>
                    <a th:href="@{/home}" href="/home" class="btn btn-secondary">返回</a>
                </div>
            </form>
        </div>
        <div class="col"></div>
    </div>
</main>

大部分都是渲染input的代码,现在来看看关键的控制器。

GET请求控制器

这个控制器比较简单,套路代码是获取当前用户是否为管理员,以及邮件功能是否初始化,如果已经初始化就不提示内容。

    @GetMapping("/email")
    public String emailSetting(@ModelAttribute("emailSetting") EmailSetting emailSetting, Model model) {
        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        model.addAttribute("isAdmin", user.hasRole("ADMIN"));
        model.addAttribute("emailInitialized", Boolean.parseBoolean(optionService.getOptionByKey("emailInitialized").getOptionValue()));
        return "/admin/emailEdit";
    }

既然是要搭配POST来使用的,用@ModelAttribute比较方便。

POST请求控制器

这个控制器的逻辑是先验证表单,通过之后,交给业务层去发送一个测试邮件,如果发送成功,就更新邮件服务设置,如果失败,就不会更新。逻辑如下:

@PostMapping("/email")
    public String editEmail(@Valid @ModelAttribute(name = "emailSetting") EmailSetting emailSetting, BindingResult rs, Model model) {
        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        model.addAttribute("isAdmin", user.hasRole("ADMIN"));

        //返回错误页面
        if (!rs.hasErrors()) {
            // 发送测试邮件并更新服务器设置
            model.addAttribute("failed", !emailSendingService.sendTestEmailAndUpdate(emailSetting));
        }
        model.addAttribute("emailInitialized", Boolean.parseBoolean(optionService.getOptionByKey("emailInitialized").getOptionValue()));
        return "/admin/emailEdit";
    }

现在的关键就是要大改业务层,同时编写sendTestEmailAndUpdate方法。

业务层大改造

业务层的关键在于不能依据配置来组装JavaMailSenderImpl了,而必须每次发送的时候都要从数据库中读取数据组装JavaMailSenderImpl。因此我也不将其设置为一个Bean了,而是在业务层中直接写一个私有方法,用于从数据库中加载邮件配置属性并生成JavaMailSenderImpl,下边就来改造EmailSendingService,先添加一个方法:

    // 每次读取数据库设置的sender
    private JavaMailSenderImpl javaMailSender() {
        JavaMailSenderImpl emailSender = new JavaMailSenderImpl();
        emailSender.setUsername(optionDao.findByOptionKey("emailUsername").getOptionValue());
        emailSender.setPassword(optionDao.findByOptionKey("emailPassword").getOptionValue());
        emailSender.setHost(optionDao.findByOptionKey("emailHost").getOptionValue());
        emailSender.setPort(Integer.parseInt(optionDao.findByOptionKey("emailPort").getOptionValue()));
        emailSender.setDefaultEncoding(optionDao.findByOptionKey("emailEncoding").getOptionValue());
        Properties properties = new Properties();
        properties.setProperty("mail.smtp.auth", optionDao.findByOptionKey("emailAuth").getOptionValue());
        emailSender.setJavaMailProperties(properties);
        return emailSender;
    }

再添加一个方法,用于通过控制器传入的EmailSetting组装JavaMailSenderImpl,这个方法用于先发送测试邮件,看看是否成功:

    // 根据EmailSender组装的Sender
    private JavaMailSenderImpl prepareEmailSender(EmailSetting emailSetting) {
        JavaMailSenderImpl emailSenderTester = new JavaMailSenderImpl();
        emailSenderTester.setUsername(emailSetting.getEmailUsername());
        emailSenderTester.setPassword(emailSetting.getEmailPassword());
        emailSenderTester.setHost(emailSetting.getEmailHost());
        emailSenderTester.setPort(emailSetting.getEmailPort());
        emailSenderTester.setDefaultEncoding(emailSetting.getEmailEncoding());
        Properties properties = new Properties();
        properties.setProperty("mail.smtp.auth", String.valueOf(emailSetting.isEmailAuth()));
        emailSenderTester.setJavaMailProperties(properties);
        return emailSenderTester;
    }

然后就是发送测试邮件并且保存的两个方法:

    @Transactional
    public boolean sendTestEmailAndUpdate(EmailSetting emailSetting) {
        JavaMailSenderImpl emailSenderTester = prepareEmailSender(emailSetting);
        try {
            SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
            simpleMailMessage.setFrom(emailSetting.getEmailUsername());
            simpleMailMessage.setTo(emailSetting.getEmailUsername());
            simpleMailMessage.setSubject("测试邮件");
            simpleMailMessage.setText("这是一封测试邮件,如果您收到这封邮件说明邮件服务器配置正确。");
            emailSenderTester.send(simpleMailMessage);
        } catch (Exception e) {
            return false;
        }
        saveEmailSetting(emailSetting);
        return true;
    }

    @Transactional
    public void saveEmailSetting(EmailSetting emailSetting) {
        Option emailUsername = optionDao.findByOptionKey("emailUsername");
        Option emailPassword = optionDao.findByOptionKey("emailPassword");
        Option emailHost = optionDao.findByOptionKey("emailHost");
        Option emailPort = optionDao.findByOptionKey("emailPort");
        Option emailEncoding = optionDao.findByOptionKey("emailEncoding");
        Option emailAuth = optionDao.findByOptionKey("emailAuth");
        emailUsername.setOptionValue(emailSetting.getEmailUsername());
        emailPassword.setOptionValue(emailSetting.getEmailPassword());
        emailHost.setOptionValue(emailSetting.getEmailHost());
        emailPort.setOptionValue(String.valueOf(emailSetting.getEmailPort()));
        emailEncoding.setOptionValue(emailSetting.getEmailEncoding());
        emailAuth.setOptionValue(String.valueOf(emailSetting.isEmailAuth()));
        optionDao.save(emailUsername);
        optionDao.save(emailPassword);
        optionDao.save(emailHost);
        optionDao.save(emailPort);
        optionDao.save(emailEncoding);
        optionDao.save(emailAuth);
        //更新邮件初始化状态
        Option emailInitialized = optionDao.findByOptionKey("emailInitialized");
        if (!Boolean.parseBoolean(emailInitialized.getOptionValue())) {
            emailInitialized.setOptionValue(String.valueOf(true));
            optionDao.save(emailInitialized);
        }

这样改造之后,在ProjectConfig中删除JavaMailSenderBean,每次都动态生成新的JavaMailSender。之前的发送重置密码邮件的方法也需要略微修改,每次都使用新的JavaMailSender

@Transactional
public boolean sendMimeMessage(String username, String email, String path) {
    JavaMailSenderImpl emailSender = javaMailSender();
    MimeMessage mimeMessage = emailSender.createMimeMessage();
    String message = template
            .replace("{{username}}", username)
            .replace("{{link}}", emailProperties.getPath() + path)
            .replace("{{interval}}", emailProperties.getInvalidInterval())
            .replace("{{mail}}", emailProperties.getFrom())
            .replace("{{time}}", simpleDateFormat.format(new Date()));
    try {
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
        helper.setTo(email);
        helper.setFrom(optionDao.findByOptionKey("emailUsername").getOptionValue());
        helper.setSubject("用户密码重置");
        helper.setText(message, true);
        emailSender.send(mimeMessage);
        return true;
    } catch (Exception e) {
        return false;
    }
}

到这里功能就编写完了,页面长这个样子:
fms-email

这里特别要注意helper.setFrom必须要设置成和电子邮件用户名一样,否则会发送错误,所以也要动态获取。所以之前在application.properties中的一条配置没有用了。下边就是把这两个东西也写入到数据库中去。其本质和邮件配置没有什么不同。

LICENSED UNDER CC BY-NC-SA 4.0
Comment