Spring Security - Réinitialiser votre mot de passe

Spring Security - Réinitialiser votre mot de passe

1. Vue d'ensemble

Dans ce didacticiel - nous poursuivons lesRegistration with Spring Security series en cours en examinantthe basic “I forgot my password” feature - afin que l'utilisateur puisse réinitialiser en toute sécurité son propre mot de passe lorsqu'il en a besoin.

2. Le jeton de réinitialisation de mot de passe

Commençons par créer une entitéPasswordResetToken pour l'utiliser pour réinitialiser le mot de passe de l'utilisateur:

@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;
}

Lorsqu'une réinitialisation de mot de passe est déclenchée - un jeton sera créé eta special link containing this token will be emailed to the user.

Le jeton et le lien ne seront valides que pour une période de temps définie (24 heures dans cet exemple).

3. forgotPassword.html

La première page du processus estthe “I forgot my password” page - où l'utilisateur est invité à entrer son adresse e-mail pour que le processus de réinitialisation puisse démarrer.

Alors, créons un simpleforgotPassword.html demandant à l'utilisateur une adresse e-mail:



    

reset

registration login

Nous devons maintenant créer un lien vers cette nouvelle page «reset password» depuis la page de connexion:

4. Créer lesPasswordResetToken

Commençons par créer le nouveauPasswordResetToken et envoyez-le par e-mail à l'utilisateur:

@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()));
}

Et voici la méthodecreatePasswordResetTokenForUser():

public void createPasswordResetTokenForUser(User user, String token) {
    PasswordResetToken myToken = new PasswordResetToken(token, user);
    passwordTokenRepository.save(myToken);
}

Et voici la méthodeconstructResetTokenEmail() - utilisée pour envoyer un e-mail avec le jeton de réinitialisation:

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;
}

Notez comment nous avons utilisé un simple objetGenericResponse pour représenter notre réponse au client:

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. Traiter lesPasswordResetToken

L'utilisateur reçoit le courrier électronique avec le lien unique permettant de réinitialiser son mot de passe, puis clique sur le lien:

@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();
}

Et voici la méthodevalidatePasswordResetToken():

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;
}

Comme vous pouvez le voir - si le jeton est valide, l'utilisateur sera autorisé à changer son mot de passe en lui accordant unCHANGE_PASSWORD_PRIVILEGE, et à le diriger vers une page pour mettre à jour son mot de passe.

La note intéressante ici est - ce nouveau privilège ne sera utilisable que pour changer le mot de passe (comme son nom l’indique) - et il est donc sûr de l’attribuer par programme à l’utilisateur.

6. Changer le mot de passe

À ce stade, l'utilisateur voit la pagePassword Reset simple - où la seule option possible est deprovide a new password:

6.1. updatePassword.html



reset

6.2. Enregistrer le mot de passe de l'utilisateur

Enfin, lorsque la demande de publication précédente est soumise, le nouveau mot de passe de l'utilisateur est enregistré:

@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));
}

Et voici la méthodechangeUserPassword():

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

Notez que nous sécurisons les demandes de mise à jour et d’enregistrement des mots de passe - comme suit:

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

7. Conclusion

Dans cet article, nous avons implémenté une fonctionnalité simple mais très utile pour un processus d’authentification mature - l’option de réinitialiser votre propre mot de passe en tant qu’utilisateur du système.

Lesfull implementation de ce didacticiel se trouvent dansthe GitHub project - il s'agit d'un projet basé sur Eclipse, il devrait donc être facile à importer et à exécuter tel quel.