Spring Security - パスワードを再設定する

Spring Security –パスワードをリセットする

1. 概要

このチュートリアルでは、進行中のRegistration with Spring Security seriesを継続してthe basic “I forgot my password” featureを確認し、ユーザーが必要なときに自分のパスワードを安全にリセットできるようにします。

2. パスワードリセットトークン

ユーザーのパスワードをリセットするために使用する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;
}

パスワードのリセットがトリガーされると、トークンとa special link containing this token will be emailed to the userが作成されます。

トークンとリンクは、設定された期間(この例では24時間)のみ有効です。

3. forgotPassword.html

プロセスの最初のページはthe “I forgot my password” pageです。実際のリセットプロセスを開始するために、ユーザーは電子メールアドレスの入力を求められます。

それでは、ユーザーにメールアドレスを尋ねる簡単なforgotPassword.htmlを作成しましょう。



    

reset

registration login

ログインページからこの新しい「reset password」ページにリンクする必要があります。

4. PasswordResetTokenを作成します

新しいPasswordResetTokenを作成し、それを電子メールでユーザーに送信することから始めましょう。

@RequestMapping(value = "/user/resetPassword",
                method = RequestMethod.POST)
@ResponseBody
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?id=" +
      user.getId() + "&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;
    }
}

5. PasswordResetTokenを処理します

ユーザーは、パスワードをリセットするための一意のリンクが記載された電子メールを受け取り、リンクをクリックします。

@RequestMapping(value = "/user/changePassword", method = RequestMethod.GET)
public String showChangePasswordPage(Locale locale, Model model,
  @RequestParam("id") long id, @RequestParam("token") String token) {
    String result = securityService.validatePasswordResetToken(id, token);
    if (result != null) {
        model.addAttribute("message",
          messages.getMessage("auth.message." + result, null, locale));
        return "redirect:/login?lang=" + locale.getLanguage();
    }
    return "redirect:/updatePassword.html?lang=" + locale.getLanguage();
}

そしてここにvalidatePasswordResetToken()メソッドがあります:

public String validatePasswordResetToken(long id, String token) {
    PasswordResetToken passToken =
      passwordTokenRepository.findByToken(token);
    if ((passToken == null) || (passToken.getUser()
        .getId() != id)) {
        return "invalidToken";
    }

    Calendar cal = Calendar.getInstance();
    if ((passToken.getExpiryDate()
        .getTime() - cal.getTime()
        .getTime()) <= 0) {
        return "expired";
    }

    User user = passToken.getUser();
    Authentication auth = new UsernamePasswordAuthenticationToken(
      user, null, Arrays.asList(
      new SimpleGrantedAuthority("CHANGE_PASSWORD_PRIVILEGE")));
    SecurityContextHolder.getContext().setAuthentication(auth);
    return null;
}

ご覧のとおり、トークンが有効な場合、ユーザーはCHANGE_PASSWORD_PRIVILEGEを付与してパスワードを変更し、パスワードを更新するページに誘導することができます。

ここで興味深いのは、この新しい特権は(名前が示すように)パスワードを変更するためにのみ使用できるため、ユーザーにプログラムで許可することは安全です。

6. パスワードを変更する

この時点で、ユーザーには単純なPassword Resetページが表示されます。可能なオプションはprovide a new passwordのみです。

6.1. updatePassword.html



reset

6.2. ユーザーパスワードを保存する

最後に、前の投稿リクエストが送信されると、新しいユーザーパスワードが保存されます。

@RequestMapping(value = "/user/savePassword", method = RequestMethod.POST)
@ResponseBody
public GenericResponse savePassword(Locale locale,
  @Valid PasswordDto passwordDto) {
    User user =
      (User) SecurityContextHolder.getContext()
                                  .getAuthentication().getPrincipal();

    userService.changeUserPassword(user, passwordDto.getNewPassword());
    return new GenericResponse(
      messages.getMessage("message.resetPasswordSuc", null, locale));
}

そしてここにchangeUserPassword()メソッドがあります:

public void changeUserPassword(User user, String password) {
    user.setPassword(passwordEncoder.encode(password));
    repository.save(user);
}

次のように、更新を保護し、パスワードリクエストを保存していることに注意してください。

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/user/updatePassword*",
                     "/user/savePassword*",
                     "/updatePassword*")
        .hasAuthority("CHANGE_PASSWORD_PRIVILEGE")
...

7. 結論

この記事では、成熟した認証プロセスのためのシンプルだが非常に便利な機能を実装しました。これは、システムのユーザーとして自分のパスワードをリセットするオプションです。

このチュートリアルのfull implementationは、the GitHub projectにあります。これはEclipseベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。