Biên Dịch : Nguyễn Hoàng Đạt - Lớp Java07
Email : hoangdat3179@gmail.com
Bài viết gốc : https://www.baeldung.com/spring-security-registration-i-forgot-my-password

1.Tổng quan

Trong bài viết này – chúng ta đang tiếp tục series Registration with Spring Security đang diễn ra với cái nhìn về tính năng cơ bản ‘Quên mật khẩu’ – để người dùng có thể đặt lại password của chính mình một cách an toàn khi họ cần.

2. Yêu cầu thiết lập lại mật khẩu của bạn

Quy trình đặt lại mật khẩu thường bắt đầu khi người dùng nhấp vào một số loại nút ‘đặt lại’ trên trang Đăng nhập. Sau đó, chúng tôi có thể yêu cầu người dùng cung cấp địa chỉ email của họ hoặc thông tin nhận dạng khác. Sau khi xác nhận, chúng tôi có thể tạo mã thông báo và gửi email cho người dùng. Sơ đồ sau đây trực quan hóa quy trình mà chúng ta sẽ triển khai trong bài viết này:

forgot pass

3.Mã thông báo đặt lại mật khẩu

Hãy bắt đầu bằng cách tạo một thực thể PasswordResetToken để sử dụng nó cho việc đặt lại mật khẩu của người dùng:

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

Khi đặt lại mật khẩu được kích hoạt – một mã thông báo sẽ được tạo và một liên kết đặc biệt có chứa mã thông báo này sẽ được gửi qua email cho người dùng.

Mã thông báo và liên kết sẽ chỉ có hiệu lực trong một khoảng thời gian nhất định (24 giờ trong ví dụ này).

4. forgotPassword.html

Trang đầu tiên trong quá trình này là trang ‘Tôi quên mật khẩu của mình’ - nơi người dùng được nhắc nhập địa chỉ email của họ để quá trình đặt lại thực tế bắt đầu.

Vì vậy - hãy tạo một mật khẩu quên đơn giản.html yêu cầu người dùng cung cấp địa chỉ email:

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

Bây giờ chúng ta cần liên kết đến trang ‘đặt lại mật khẩu’ mới này từ trang đăng nhập:

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

5.Khởi tạo PasswordResetToken

Hãy bắt đầu bằng cách tạo PasswordResetToken mới và gửi nó qua email cho người dùng:

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

Và đây là phương thức createPasswordResetTokenForUser():

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

Và đây là phương thức constructResetTokenEmail() – dùng để gửi email với reset token:

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

Lưu ý cách chúng tôi sử dụng một đối tượng đơn giản GenericResponse để thể hiện phản hồi của chúng tôi với khách hàng:

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. Kiểm tra PasswordResetToken

Khi người dùng nhấp vào liên kết trong email của họ user/changePassword:

  • xác minh rằng mã thông báo là hợp lệ
  • giới thiệu cho người dùng trang updatePassword, nơi họ có thể nhập mật khẩu mới

Mật khẩu mới và mã thông báo sau đó được chuyển đến user/savePassword:

reset

Người dùng nhận được email có liên kết duy nhất để đặt lại mật khẩu của họ và nhấp vào liên kết:

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

Và đây là phương thức 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. Thay đổi mật khẩu

Tại thời điểm này, người dùng thấy trang Đặt lại mật khẩu đơn giản - trong đó tùy chọn duy nhất có thể là cung cấp mật khẩu mới:

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>

Lưu ý rằng chúng tôi hiển thị mã thông báo đặt lại và chuyển nó dưới dạng tham số POST trong lệnh gọi sau để lưu mật khẩu.

7.2. Lưu mật khẩu

Cuối cùng, khi yêu cầu bài viết trước được gửi - mật khẩu người dùng mới được lưu:

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

Và đây là phương thức ChangeUserPassword():

public void changeUserPassword(User user, String password) {
    user.setPassword(passwordEncoder.encode(password));
    repository.save(user);
}

và PasswordDto:

public class PasswordDto {

    private String oldPassword;

    private  String token;

    @ValidPassword
    private String newPassword;
}

8.Tổng kết

Trong bài viết này chúng ta đã học được cách xây dựng tính năng đặt lại mật khẩu trong Spring Security.