Перейти к основному содержимому

Spring Security — сброс пароля

· 6 мин. чтения

1. Обзор

В этом руководстве — мы продолжаем текущую серию « Регистрация в Spring Security » , рассматривая базовую функцию « Я забыл свой пароль » , чтобы пользователь мог безопасно сбросить свой пароль, когда ему это нужно.

2. Запросите сброс вашего пароля

Поток сброса пароля обычно начинается, когда пользователь нажимает кнопку «сброс» на странице входа. Затем мы можем запросить у пользователя его адрес электронной почты или другую идентифицирующую информацию. После подтверждения мы можем сгенерировать токен и отправить электронное письмо пользователю.

Следующая диаграмма визуализирует поток, который мы реализуем в этой статье:

./56114ab3f4baeea02f7844d4d10be4b6.png

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;
}

При сбросе пароля будет создан токен и специальная ссылка, содержащая этот токен, будет отправлена пользователю по электронной почте .

Токен и ссылка будут действительны только в течение установленного периода времени (в данном примере 24 часа).

4. забыл пароль.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>

Теперь нам нужно сослаться на эту новую страницу « сбросить пароль » со страницы входа:

<a th:href="@{/forgetPassword.html}" 
th:text="#{message.resetPassword}">reset</a>

5. Создайте токен PasswordResetToken

Давайте начнем с создания нового 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()));
}

А вот метод createPasswordResetTokenForUser() :

public void createPasswordResetTokenForUser(User user, String token) {
PasswordResetToken myToken = new PasswordResetToken(token, user);
passwordTokenRepository.save(myToken);
}

А вот методstructResetTokenEmail () — используется для отправки электронного письма с токеном сброса:

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;
}

Обратите внимание, как мы использовали простой объект 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. Проверьте токен PasswordResetToken

Как только пользователь щелкнет ссылку в своем письме, конечная точка user/changePassword :

  • проверяет, что токен действителен и
  • представляет пользователю страницу updatePassword , где он может ввести новый пароль

Затем новый пароль и токен передаются в конечную точку user/savePassword :

./181acc5b5d7cb7a14b2f5fd9bd278077.png

Пользователь получает электронное письмо с уникальной ссылкой для сброса пароля и переходит по ссылке:

@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. Изменить пароль

В этот момент пользователь видит простую страницу сброса пароля , где единственным возможным вариантом является предоставление нового пароля :

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>

Обратите внимание, что мы показываем токен сброса и передаем его в качестве параметра POST в следующем вызове, чтобы сохранить пароль.

7.2. Сохранить пароль

Наконец, при отправке предыдущего почтового запроса новый пароль пользователя сохраняется:

@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));
}
}

А вот метод 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. Заключение

В этой статье мы реализовали простую, но очень полезную функцию для зрелого процесса аутентификации — возможность сбросить собственный пароль как пользователя системы.

Полную реализацию этого руководства можно найти в проекте GitHub — это проект на основе Eclipse, поэтому его легко импортировать и запускать как есть.