1. 概述
在本教程中,我们将继续《使用 Spring Security 实现注册》系列文章,重点讨论如何在用户注册链接过期后重新发送验证邮件。
这是个很常见的需求:用户没有及时激活账号,验证链接已失效,此时用户需要一个新的验证链接。我们可以通过重新生成 token 并发送新邮件来实现。
2. 重新发送验证链接
当用户请求一个新的验证链接时,我们需要做两件事:
✅ 重置当前 token 的过期时间
✅ 重新发送验证邮件
下面是具体的实现代码:
@GetMapping("/user/resendRegistrationToken")
public GenericResponse resendRegistrationToken(
HttpServletRequest request, @RequestParam("token") String existingToken) {
VerificationToken newToken = userService.generateNewVerificationToken(existingToken);
User user = userService.getUser(newToken.getToken());
String appUrl =
"http://" + request.getServerName() +
":" + request.getServerPort() +
request.getContextPath();
SimpleMailMessage email =
constructResendVerificationTokenEmail(appUrl, request.getLocale(), newToken, user);
mailSender.send(email);
return new GenericResponse(
messages.getMessage("message.resendToken", null, request.getLocale()));
}
构造邮件内容的方法如下:
private SimpleMailMessage constructResendVerificationTokenEmail
(String contextPath, Locale locale, VerificationToken newToken, User user) {
String confirmationUrl =
contextPath + "/registrationConfirm.html?token=" + newToken.getToken();
String message = messages.getMessage("message.resendToken", null, locale);
SimpleMailMessage email = new SimpleMailMessage();
email.setSubject("重新发送注册验证链接");
email.setText(message + "\n" + confirmationUrl);
email.setFrom(env.getProperty("support.email"));
email.setTo(user.getEmail());
return email;
}
⚠️ 注意:拼接 URL 时建议使用 UriComponentsBuilder
更安全,但为了保持示例简单,这里直接拼接。
另外,我们需要在验证 token 时,判断 token 是否已过期,并在页面上提示用户可以重新发送验证链接:
@GetMapping("/registrationConfirm")
public String confirmRegistration(
Locale locale, Model model, @RequestParam("token") String token) {
VerificationToken verificationToken = userService.getVerificationToken(token);
if (verificationToken == null) {
String message = messages.getMessage("auth.message.invalidToken", null, locale);
model.addAttribute("message", message);
return "redirect:/badUser.html?lang=" + locale.getLanguage();
}
User user = verificationToken.getUser();
Calendar cal = Calendar.getInstance();
if ((verificationToken.getExpiryDate().getTime() - cal.getTime().getTime()) <= 0) {
model.addAttribute("message", messages.getMessage("auth.message.expired", null, locale));
model.addAttribute("expired", true);
model.addAttribute("token", token);
return "redirect:/badUser.html?lang=" + locale.getLanguage();
}
user.setEnabled(true);
userService.saveRegisteredUser(user);
model.addAttribute("message", messages.getMessage("message.accountVerified", null, locale));
return "redirect:/login.html?lang=" + locale.getLanguage();
}
3. 异常处理
上述功能在执行过程中可能抛出异常,比如用户不存在、邮件配置错误等,因此我们需要一个统一的异常处理器。
我们使用 @ControllerAdvice
来处理全局异常,这样可以统一返回格式:
@ControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@Autowired
private MessageSource messages;
@ExceptionHandler({ UserNotFoundException.class })
public ResponseEntity<Object> handleUserNotFound(RuntimeException ex, WebRequest request) {
logger.error("404 Status Code", ex);
GenericResponse bodyOfResponse = new GenericResponse(
messages.getMessage("message.userNotFound", null, request.getLocale()), "UserNotFound");
return handleExceptionInternal(
ex, bodyOfResponse, new HttpHeaders(), HttpStatus.NOT_FOUND, request);
}
@ExceptionHandler({ MailAuthenticationException.class })
public ResponseEntity<Object> handleMail(RuntimeException ex, WebRequest request) {
logger.error("500 Status Code", ex);
GenericResponse bodyOfResponse = new GenericResponse(
messages.getMessage(
"message.email.config.error", null, request.getLocale()), "MailError");
return handleExceptionInternal(
ex, bodyOfResponse, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
}
@ExceptionHandler({ Exception.class })
public ResponseEntity<Object> handleInternal(RuntimeException ex, WebRequest request) {
logger.error("500 Status Code", ex);
GenericResponse bodyOfResponse = new GenericResponse(
messages.getMessage(
"message.error", null, request.getLocale()), "InternalError");
return handleExceptionInternal(
ex, bodyOfResponse, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
}
}
其中,GenericResponse
是一个通用的响应对象:
public class GenericResponse {
private String message;
private String error;
public GenericResponse(String message) {
this.message = message;
}
public GenericResponse(String message, String error) {
this.message = message;
this.error = error;
}
}
4. 修改 badUser.html 页面
当 token 过期时,我们希望用户看到一个重新发送验证链接的按钮。因此,我们需要修改 badUser.html
页面:
<html>
<head>
<title th:text="#{label.badUser.title}">bad user</title>
</head>
<body>
<h1 th:text="${param.message[0]}">error</h1>
<br>
<a th:href="@{/user/registration}" th:text="#{label.form.loginSignUp}">
注册新账号
</a>
<div th:if="${param.expired[0]}">
<h1 th:text="#{label.form.resendRegistrationToken}">重新发送验证链接</h1>
<button onclick="resendToken()"
th:text="#{label.form.resendRegistrationToken}">发送</button>
<script src="jquery.min.js"></script>
<script type="text/javascript">
var serverContext = [[@{/}]];
function resendToken(){
var token = [[${param.token[0]}]];
$.get(serverContext + "user/resendRegistrationToken?token=" + token,
function(data){
window.location.href =
serverContext + "login.html?message=" + encodeURIComponent(data.message);
})
.fail(function(data) {
if(data.responseJSON.error.indexOf("MailError") > -1) {
window.location.href = serverContext + "emailError.html";
}
else {
window.location.href =
serverContext + "login.html?message=" + encodeURIComponent(data.responseJSON.message);
}
});
}
</script>
</div>
</body>
</html>
⚠️ 注意:这里使用了 jQuery 发送 GET 请求,并根据响应结果跳转页面。实际项目中建议使用 POST 方法,并考虑 CSRF 保护。
5. 总结
在本教程中,我们实现了用户请求新的验证链接的功能。这在实际项目中非常实用,尤其是在用户忘记及时激活账号的情况下。
完整代码可以在 GitHub 上找到:spring-security-registration(这是一个基于 Eclipse 的项目,可直接导入运行)
如果你正在构建一个完整的注册系统,这个功能是必不可少的。记得处理好异常、邮件配置、以及 token 的生命周期管理。