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:
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:
Copy @ 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:
Copy < 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:
Copy < 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:
Copy @ 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()
:
Copy 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:
Copy 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:
Copy 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
:
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:
Copy @ 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()
:
Copy 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
Copy < 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:
Copy @ 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()
:
Copy public void changeUserPassword( User user , String password) {
user . setPassword ( passwordEncoder . encode (password));
repository . save (user);
}
và PasswordDto:
Copy public class PasswordDto {
private String oldPassword;
private String token;
@ ValidPassword
private String newPassword;
}
8.Tham khảo
Spring Security – Reset Your Password