1. Overview
1.概述
In this tutorial – we’re continuing the ongoing Registration with Spring Security series with a look at the basic “I forgot my password” feature – so that the user can safely reset their own password when they need to.
在本教程中,我们将继续正在进行的用Spring Security注册系列,看看基本的”我忘了我的密码“功能–以便用户在需要时可以安全地重置自己的密码。
2. Request the Reset of Your Password
2.要求重设你的密码
A password reset flow typically starts when the user clicks some kind of “reset” button on the Login page. Then, we can ask the user for their email address or other identifying information. Once confirmed, we can generate a token and send an email to the user.
一个密码重置流程通常在用户点击登录页面上的某种 “重置 “按钮时开始。然后,我们可以要求用户提供他们的电子邮件地址或其他识别信息。一旦确认,我们可以生成一个令牌,并向用户发送一封电子邮件。
The following diagram visualizes the flow that we’ll implement in this article:
下图是我们将在本文中实现的流程的可视化图。
3. The Password Reset Token
3.密码重置令牌
Let’s start by creating a PasswordResetToken entity to use it for resetting the user’s password:
让我们先创建一个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;
}
When a password reset is triggered – a token will be created and a special link containing this token will be emailed to the user.
当密码重置被触发时–将创建一个令牌,一个包含该令牌的特殊链接将通过电子邮件发送给用户。
The token and the link will only be valid for a set period of time (24 hours in this example).
令牌和链接只在设定的时间段内有效(本例中为24小时)。
4. forgotPassword.html
4.forgotPassword.html
The first page in the process is the “I forgot my password” page – where the user is prompted for their email address in order for the actual reset process to start.
这个过程中的第一个页面是“我忘记了我的密码“页面–在这里用户被提示提供他们的电子邮件地址,以便开始实际的重置过程。
So – let’s craft a simple forgotPassword.html asking the user for an email address:
所以–让我们制作一个简单的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>
We now need to link to this new “reset password” page from the login page:
我们现在需要从登录页面链接到这个新的”重设密码“页面。
<a th:href="@{/forgetPassword.html}"
th:text="#{message.resetPassword}">reset</a>
5. Create the PasswordResetToken
5.创建PasswordResetToken
Let’s start by creating the new PasswordResetToken and send it via email to the user:
让我们开始创建新的PasswordResetToken,并通过电子邮件将其发送给用户。
@PostMapping("/user/resetPassword")
public GenericResponse resetPassword(HttpServletRequest request,
@RequestParam("email") String userEmail) {
User user = userService.findUserByEmail(userEmail);
if (user == null) {
throw new UserNotFoundException();
}
String token = UUID.randomUUID().toString();
userService.createPasswordResetTokenForUser(user, token);
mailSender.send(constructResetTokenEmail(getAppUrl(request),
request.getLocale(), token, user));
return new GenericResponse(
messages.getMessage("message.resetPasswordEmail", null,
request.getLocale()));
}
And here is the createPasswordResetTokenForUser() method:
这里是createPasswordResetTokenForUser()方法。
public void createPasswordResetTokenForUser(User user, String token) {
PasswordResetToken myToken = new PasswordResetToken(token, user);
passwordTokenRepository.save(myToken);
}
And here is method constructResetTokenEmail() – used to send an email with the reset token:
这里是方法constructResetTokenEmail() – 用于发送带有重置令牌的电子邮件。
private SimpleMailMessage constructResetTokenEmail(
String contextPath, Locale locale, String token, User user) {
String url = contextPath + "/user/changePassword?token=" + token;
String message = messages.getMessage("message.resetPassword",
null, locale);
return constructEmail("Reset Password", message + " \r\n" + url, user);
}
private SimpleMailMessage constructEmail(String subject, String body,
User user) {
SimpleMailMessage email = new SimpleMailMessage();
email.setSubject(subject);
email.setText(body);
email.setTo(user.getEmail());
email.setFrom(env.getProperty("support.email"));
return email;
}
Note how we used a simple object GenericResponse to represent our response to the client:
注意我们如何使用一个简单的对象GenericResponse来表示我们对客户端的响应。
public class GenericResponse {
private String message;
private String error;
public GenericResponse(String message) {
super();
this.message = message;
}
public GenericResponse(String message, String error) {
super();
this.message = message;
this.error = error;
}
}
6. Check the PasswordResetToken
6.检查PasswordResetToken
Once the user clicks on the link in their email, the user/changePassword endpoint:
一旦用户点击他们电子邮件中的链接,user/changePassword端点。
- verifies that the token is valid and
- presents the user with the updatePassword page, where he can enter a new password
The new password and the token are then passed to the user/savePassword endpoint:
然后,新密码和令牌被传递到user/savePassword端点:
The user gets the email with the unique link for resetting their password, and clicks the link:
用户收到带有重置密码的唯一链接的电子邮件,并点击该链接。
@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();
}
}
And here is the validatePasswordResetToken() method:
这里是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. Change the Password
7.更改密码
At this point, the user sees the simple Password Reset page – where the only possible option is to provide a new password:
在这一点上,用户看到的是简单的密码重置页面–其中唯一可能的选择是提供一个新密码。
7.1. updatePassword.html
7.1.updatePassword.html
<html>
<body>
<div sec:authorize="hasAuthority('CHANGE_PASSWORD_PRIVILEGE')">
<h1 th:text="#{message.resetYourPassword}">reset</h1>
<form>
<label th:text="#{label.user.password}">password</label>
<input id="password" name="newPassword" type="password" value="" />
<label th:text="#{label.user.confirmPass}">confirm</label>
<input id="matchPassword" type="password" value="" />
<label th:text="#{token.message}">token</label>
<input id="token" name="token" value="" />
<div id="globalError" style="display:none"
th:text="#{PasswordMatches.user}">error</div>
<button type="submit" onclick="savePass()"
th:text="#{message.updatePassword}">submit</button>
</form>
<script th:inline="javascript">
var serverContext = [[@{/}]];
$(document).ready(function () {
$('form').submit(function(event) {
savePass(event);
});
$(":password").keyup(function(){
if($("#password").val() != $("#matchPassword").val()){
$("#globalError").show().html(/*[[#{PasswordMatches.user}]]*/);
}else{
$("#globalError").html("").hide();
}
});
});
function savePass(event){
event.preventDefault();
if($("#password").val() != $("#matchPassword").val()){
$("#globalError").show().html(/*[[#{PasswordMatches.user}]]*/);
return;
}
var formData= $('form').serialize();
$.post(serverContext + "user/savePassword",formData ,function(data){
window.location.href = serverContext + "login?message="+data.message;
})
.fail(function(data) {
if(data.responseJSON.error.indexOf("InternalError") > -1){
window.location.href = serverContext + "login?message=" + data.responseJSON.message;
}
else{
var errors = $.parseJSON(data.responseJSON.message);
$.each( errors, function( index,item ){
$("#globalError").show().html(item.defaultMessage);
});
errors = $.parseJSON(data.responseJSON.error);
$.each( errors, function( index,item ){
$("#globalError").show().append(item.defaultMessage+"<br/>");
});
}
});
}
</script>
</div>
</body>
</html>
Note that we show the reset token and pass it as a POST parameter in the following call to save the password.
注意,我们显示了重置令牌,并在下面的调用中作为POST参数传递,以保存密码。
7.2. Save the Password
7.2.保存密码
Finally, when the previous post request is submitted – the new user password is saved:
最后,当前一个帖子请求被提交时–新的用户密码被保存。
@PostMapping("/user/savePassword")
public GenericResponse savePassword(final Locale locale, @Valid PasswordDto passwordDto) {
String result = securityUserService.validatePasswordResetToken(passwordDto.getToken());
if(result != null) {
return new GenericResponse(messages.getMessage(
"auth.message." + result, null, locale));
}
Optional user = userService.getUserByPasswordResetToken(passwordDto.getToken());
if(user.isPresent()) {
userService.changeUserPassword(user.get(), passwordDto.getNewPassword());
return new GenericResponse(messages.getMessage(
"message.resetPasswordSuc", null, locale));
} else {
return new GenericResponse(messages.getMessage(
"auth.message.invalid", null, locale));
}
}
And here is the changeUserPassword() method:
而这里是changeUserPassword()方法。
public void changeUserPassword(User user, String password) {
user.setPassword(passwordEncoder.encode(password));
repository.save(user);
}
And the PasswordDto:
还有PasswordDto。
public class PasswordDto {
private String oldPassword;
private String token;
@ValidPassword
private String newPassword;
}
8. Conclusion
8.结论
In this article, we implemented a simple but very useful feature for a mature Authentication process – the option to reset your own password, as a user of the system.
在这篇文章中,我们为一个成熟的认证过程实现了一个简单但非常有用的功能–作为系统的用户,可以选择重置自己的密码。
The full implementation of this tutorial can be found in the GitHub project – this is an Eclipse based project, so it should be easy to import and run as it is.
本教程的完整实现可以在GitHub 项目中找到 – 这是一个基于 Eclipse 的项目,因此应该很容易导入并按原样运行。