在目前的系统中,尚未初始化管理员的时候,是禁止用户注册的。所以不存在用户能够重置密码的情况,因为系统中还没没有用户。一旦完成了初始化管理员,假如管理员忘记了密码,就可以通过邮件重置。不过目前的邮件服务器我写死在了配置文件中,其实应该让用户输入自己的邮件服务器地址或者采用默认,这样才行。我打算把邮件相关的配置,也放入数据库中,然后让管理员来进行配置。发送邮件失败反正都是联系管理员,所以还要给管理员一个设置用户密码的界面,这个加在修改用户信息的部分就可以。
给管理员添加修改用户密码的功能
这个很简单,只需要多一个输入框即可,不过要提示别有事没事去更改其他用户的密码。页面上添加一个输入框如下:
<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
表中,在初始化的时候写入我的邮件服务器地址,然后让管理员来自行修改。在发送邮件的时候从数据库中取出对应的属性来发送邮件。
项目启动时写入邮件发送地址
邮件服务器需要如下几个属性:
- 用户名,也就是电子邮件地址
- 密码
SMTP
服务器的地址- 端口
- 编码
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
中删除JavaMailSender
的Bean
,每次都动态生成新的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;
}
}
到这里功能就编写完了,页面长这个样子:
这里特别要注意helper.setFrom
必须要设置成和电子邮件用户名一样,否则会发送错误,所以也要动态获取。所以之前在application.properties
中的一条配置没有用了。下边就是把这两个东西也写入到数据库中去。其本质和邮件配置没有什么不同。