1. 概述
在本教程中,我们将继续《使用 Spring Security 进行注册》系列文章,重点实现一个非常实用的功能 —— “忘记密码”流程。这个功能允许用户在忘记密码时,安全地重置自己的密码。
2. 触发密码重置请求
密码重置流程通常从用户点击登录页上的“重置密码”按钮开始。接着,我们需要用户提供注册时使用的邮箱地址或其他身份识别信息。确认无误后,系统将生成一个一次性令牌(Token),并通过邮件发送给用户。
下图展示了我们将实现的完整流程:
3. 密码重置令牌
我们首先创建一个名为 PasswordResetToken
的实体类,用于实现密码重置逻辑:
@Entity
public class PasswordResetToken {
private static final int EXPIRATION = 60 * 24;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String token;
@OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
@JoinColumn(nullable = false, name = "user_id")
private User user;
private Date expiryDate;
}
当用户触发密码重置请求后,系统会生成一个 Token,并通过邮件发送包含该 Token 的重置链接。
该 Token 及链接只在指定时间内有效(本例中为 24 小时)。
4. 忘记密码页面 (forgotPassword.html)
这是整个流程的第一个页面,用于提示用户输入注册时使用的邮箱地址,从而触发密码重置流程。
以下是一个简单的 forgotPassword.html
页面:
<html>
<body>
<h1 th:text="#{message.resetPassword}">reset</h1>
<label th:text="#{label.user.email}">email</label>
<input id="email" name="email" type="email" value="" />
<button type="submit" onclick="resetPass()"
th:text="#{message.resetPassword}">reset</button>
<a th:href="@{/registration.html}" th:text="#{label.form.loginSignUp}">
registration
</a>
<a th:href="@{/login}" th:text="#{label.form.loginLink}">login</a>
<script src="jquery.min.js"></script>
<script th:inline="javascript">
var serverContext = [[@{/}]];
function resetPass(){
var email = $("#email").val();
$.post(serverContext + "user/resetPassword",{email: email} ,
function(data){
window.location.href =
serverContext + "login?message=" + data.message;
})
.fail(function(data) {
if(data.responseJSON.error.indexOf("MailError") > -1)
{
window.location.href = serverContext + "emailError.html";
}
else{
window.location.href =
serverContext + "login?message=" + data.responseJSON.message;
}
});
}
</script>
</body>
</html>
5. 邮件发送逻辑
当用户提交邮箱后,后端会生成 Token,并调用邮件服务将重置链接发送给用户。示例代码如下:
@PostMapping("/user/resetPassword")
public ResponseEntity<?> resetPassword(HttpServletRequest request,
@RequestParam("email") String email) {
User user = userService.findUserByEmail(email);
if (user == null) {
throw new MailSendException("User not found");
}
String token = UUID.randomUUID().toString();
securityService.createPasswordResetTokenForUser(user, token);
String appUrl = request.getScheme() + "://" + request.getServerName();
String message = messages.getMessage("message.resetPassword", null, Locale.getDefault());
SimpleMailMessage emailTemplate = mailConstructor.constructEmail(
"Reset Password", message + " \r\n" + appUrl + "/user/changePassword?token=" + token, user.getEmail());
mailSender.send(emailTemplate);
return ResponseEntity.ok().build();
}
⚠️ 注意:邮件发送失败时,需要妥善处理异常,避免暴露用户信息。
6. 处理重置链接
当用户点击邮件中的链接后,会跳转到 /user/changePassword
接口进行 Token 验证,并跳转到设置新密码的页面。
@GetMapping("/user/changePassword")
public String showChangePasswordPage(Locale locale, Model model,
@RequestParam("token") String token) {
String result = securityService.validatePasswordResetToken(token);
if(result != null) {
String message = messages.getMessage("auth.message." + result, null, locale);
return "redirect:/login.html?lang="
+ locale.getLanguage() + "&message=" + message;
} else {
model.addAttribute("token", token);
return "redirect:/updatePassword.html?lang=" + locale.getLanguage();
}
}
下面是 validatePasswordResetToken()
方法的实现:
public String validatePasswordResetToken(String token) {
final PasswordResetToken passToken = passwordTokenRepository.findByToken(token);
return !isTokenFound(passToken) ? "invalidToken"
: isTokenExpired(passToken) ? "expired"
: null;
}
private boolean isTokenFound(PasswordResetToken passToken) {
return passToken != null;
}
private boolean isTokenExpired(PasswordResetToken passToken) {
final Calendar cal = Calendar.getInstance();
return passToken.getExpiryDate().before(cal.getTime());
}
7. 更新密码
用户进入密码更新页面后,输入新密码并提交。后端验证 Token 并更新密码:
@PostMapping("/user/savePassword")
public String savePassword(Locale locale, @RequestParam("token") String token,
@RequestBody PasswordDto passwordDto) {
String result = securityService.validatePasswordResetToken(token);
if(result != null) {
String message = messages.getMessage("auth.message." + result, null, locale);
return "redirect:/login.html?lang=" + locale.getLanguage() + "&message=" + message;
}
User user = passwordTokenRepository.findByToken(token).getUser();
securityService.changeUserPassword(user, passwordDto.getNewPassword());
String message = messages.getMessage("message.resetPasswordSuccess", null, locale);
return "redirect:/login.html?lang=" + locale.getLanguage() + "&message=" + message;
}
下面是 changeUserPassword()
方法:
public void changeUserPassword(User user, String password) {
user.setPassword(passwordEncoder.encode(password));
repository.save(user);
}
同时我们定义一个 PasswordDto
来接收用户输入:
public class PasswordDto {
private String oldPassword;
private String token;
@ValidPassword
private String newPassword;
}
8. 总结
通过本教程,我们实现了一个成熟认证系统中常见的功能 —— 用户密码重置。虽然流程看似简单,但其中涉及到 Token 管理、邮件发送、安全验证等多个关键环节,每一步都需要仔细处理,确保安全性和用户体验。
完整的实现代码可在 GitHub 上查看:完整项目地址 ✅
如果你正在构建一个基于 Spring Security 的认证系统,建议将该流程作为标准模块进行封装,方便复用和维护。