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, поэтому его должно быть легко импортировать и запускать как есть.