Spring Security 重置密码

1. 概述

在本教程中—看看基本的我忘记了我的密码功能—以便用户可以在需要时安全地重置自己的密码。

2. 请求重置密码

密码重置流程通常在用户单击登录页面上的某种“重置”按钮时启动。然后,我们可以要求用户提供他们的电子邮件地址或其他身份信息。确认后,我们可以生成令牌并向用户发送电子邮件。

下图可视化了我们将在本文中实现的流程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QA17Nxri-1671538204430)(Spring Security 重置密码.assets/image-20221220155831484.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. forgotPassword.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);
}

这是方法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端点:

  • 验证令牌是否有效并且
  • 向用户显示updatePassword页面,他可以在其中输入新密码

然后将新密码和令牌传递给user/savePassword端点:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0l9ocCmv-1671538204431)(Spring Security 重置密码.assets/image-20221220155900124.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. 保存密码

最后,在提交之前的post请求时——保存新的用户密码:

@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 的项目,因此应该很容易导入和运行。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值