SpringSecurity–パスワードをリセットする
1. 概要
このチュートリアルでは、ユーザーが安全に自分のパスワードをリセットできるように、SpringSecurityシリーズへの登録を継続して基本的な「パスワードを忘れた」機能を確認します。必要なときにパスワード。
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. 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エンドポイントに渡されます。
ユーザーは、パスワードをリセットするための一意のリンクが記載された電子メールを受け取り、次のリンクをクリックします。
@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. パスワードを保存する
最後に、前の投稿リクエストが送信されると、新しいユーザーパスワードが保存されます。
@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ベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。