Spring Security – 重置密碼

Spring Security
Remote
0
09:03 PM · Nov 29 ,2025

1. 概述

在本教程中,我們將繼續介紹Registration with Spring Security系列,重點介紹“忘記密碼”基本功能,以便用户在需要時安全地重置自己的密碼。

2. 請求重置您的密碼

密碼重置流程通常在用户在登錄頁面的某個“重置”按鈕上進行點擊開始。然後,我們可以要求用户提供他們的電子郵件地址或其他身份信息。確認後,我們可以生成一個令牌並向用户發送電子郵件。

以下圖表可視化我們將在本文中實施流程:

Request password reset e-mail

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. forgotPassword.html

第一個流程頁面是 “我忘記了密碼” 頁面 – 用户在此處會被提示其電子郵件地址,以便開始實際重置流程。

因此,讓我們創建一個簡單的 forgotPassword.html,要求用户提供電子郵件地址:

<html>
    <body>
        <h1 th:text="#{message.resetPassword}">重置密碼</h1>

        <label th:text="#{label.user.email}">郵箱</label>
        <input id="email" name="email" type="email" value="" />
        <button type="submit" onclick="resetPass()" 
            th:text="#{message.resetPassword}">重置</button>

        <a th:href="@{/registration.html}" th:text="#{label.form.loginSignUp}">
            註冊
        </a>
        <a th:href="@{/login}" th:text="#{label.form.loginLink}">
            登錄
        </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}">重置</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);
}

以下是方法 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;
}

注意我們使用簡單的對象 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 端點:

  • 驗證 token 是否有效,並
  • 向用户展示 updatePassword 頁面,其中他可以輸入新的密碼

新的密碼和 token 隨後傳遞到 user/savePassword 端點:


重置密碼

用户收到包含唯一重置密碼鏈接的郵件,並點擊該鏈接:

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

At this point, the user sees the simple page – where the only possible option is to :

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

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

public class PasswordDto {

    private String oldPassword;

    private  String token;

    @ValidPassword
    private String newPassword;
}

8. 結論

在本文中,我們為成熟的身份驗證流程實現了簡單但非常有用的功能——允許用户自行重置密碼。

下一條 »
註冊 – 密碼強度和規則
« 上一篇
註冊 API 變為 RESTful
user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.