• 使用 Spring Security 的註冊過程
• 通過電子郵件激活新賬户
• Spring Security 註冊 – 發送驗證郵件
• 使用 Spring Security 註冊 – 密碼編碼
• 註冊 API 變為 RESTful
• 更新您的密碼
• 通知用户從新設備或位置登錄
1. 概述
在本教程中,我們將繼續 Spring Security 的 “註冊”系列,重點介紹基本 “忘記密碼” 功能,以便用户在需要時安全地重置自己的密碼。
2. 請求重置您的密碼
密碼重置流程通常在用户在登錄頁面的某個“重置”按鈕上點擊時開始。然後,我們可以要求用户提供他們的電子郵件地址或其他身份驗證信息。確認後,我們可以生成一個令牌並向用户發送一封電子郵件。
以下圖表可視化了我們在本文中將要實現的流程:
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);
}以下是方法 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;
}
}檢查 密碼重置令牌
用户點擊電子郵件中的鏈接後,user/changePassword 端點會:
- 驗證令牌是否有效,
- 向用户呈現 更新密碼 頁面,其中用户可以輸入新密碼
新密碼和令牌隨後傳遞到 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());
}<div>
</div>
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));
}
}<p>以下是 <em >changeUserPassword()</em> 方法:</p>
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. 結論
本文實現了在成熟的身份驗證流程中,為用户提供自行重置密碼的簡單但非常有用的功能。