Inscription - Activez un nouveau compte par e-mail

Cet article fait partie d'une série: • Didacticiel d'enregistrement Spring Security

• Le processus d'inscription avec Spring Security

• Inscription - Activez un nouveau compte par e-mail (article actuel) • Inscription Spring Security - Renvoyez l'e-mail de vérification

• Enregistrement avec Spring Security - Codage du mot de passe

• L'API d'inscription devient RESTful

• Spring Security - Réinitialisez votre mot de passe

• Enregistrement - Force et règles du mot de passe

• Mise à jour de votre mot de passe

1. Vue d'ensemble

Cet article poursuit le cours Enregistrement auprès de la sécurité printemps série avec l' une des pièces manquantes du processus d'inscription - vérification de l'e- mail de l'utilisateur pour confirmer leur compte .

Le mécanisme de confirmation d'inscription force l'utilisateur à répondre à un e-mail « Confirmer l'inscription » envoyé après une inscription réussie pour vérifier son adresse e-mail et activer son compte. L'utilisateur le fait en cliquant sur un lien d'activation unique qui lui est envoyé par e-mail.

Suivant cette logique, un utilisateur nouvellement enregistré ne pourra pas se connecter au système tant que ce processus ne sera pas terminé.

2. Un jeton de vérification

Nous utiliserons un simple jeton de vérification comme artefact clé à travers lequel un utilisateur est vérifié.

2.1. L' entité VerificationToken

L' entité VerificationToken doit répondre aux critères suivants:

  1. Il doit renvoyer à l' Utilisateur (via une relation unidirectionnelle)
  2. Il sera créé juste après l'inscription
  3. Il expirera dans les 24 heures suivant sa création
  4. A un unique généré de façon aléatoire la valeur

Les exigences 2 et 3 font partie de la logique d'enregistrement. Les deux autres sont implémentés dans une entité VerificationToken simple comme celle de l'exemple 2.1:

Exemple 2.1.

@Entity public class VerificationToken { 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; private Date calculateExpiryDate(int expiryTimeInMinutes) { Calendar cal = Calendar.getInstance(); cal.setTime(new Timestamp(cal.getTime().getTime())); cal.add(Calendar.MINUTE, expiryTimeInMinutes); return new Date(cal.getTime().getTime()); } // standard constructors, getters and setters }

Notez le nullable = false sur l'utilisateur pour assurer l'intégrité et la cohérence des données dans l'association VerificationToken < -> User .

2.2. Ajouter le champ activé à l' utilisateur

Initialement, lorsque l' utilisateur est enregistré, ce champ activé sera défini sur faux . Pendant le processus de vérification du compte - s'il réussit - cela deviendra vrai .

Commençons par ajouter le champ à notre entité Utilisateur :

public class User { ... @Column(name = "enabled") private boolean enabled; public User() { super(); this.enabled=false; } ... }

Notez comment nous définissons également la valeur par défaut de ce champ sur false .

3. Lors de l'enregistrement du compte

Ajoutons deux éléments supplémentaires de logique métier au cas d'utilisation de l'enregistrement utilisateur:

  1. Générer le VerificationToken pour l'utilisateur et le conserver
  2. Envoyez le message électronique pour la confirmation du compte - qui comprend un lien de confirmation avec la valeur du VerificationToken

3.1. Utilisation d'un événement Spring pour créer le jeton et envoyer l'e-mail de vérification

Ces deux éléments logiques supplémentaires ne doivent pas être exécutés directement par le contrôleur car ce sont des tâches back-end «collatérales».

Le contrôleur publiera un Spring ApplicationEvent pour déclencher l'exécution de ces tâches. C'est aussi simple que d'injecter ApplicationEventPublisher , puis de l'utiliser pour publier l'achèvement de l'enregistrement.

Exemple 3.1. montre cette logique simple:

Exemple 3.1.

@Autowired ApplicationEventPublisher eventPublisher @PostMapping("/user/registration") public ModelAndView registerUserAccount( @ModelAttribute("user") @Valid UserDto userDto, HttpServletRequest request, Errors errors) { try { User registered = userService.registerNewUserAccount(userDto); String appUrl = request.getContextPath(); eventPublisher.publishEvent(new OnRegistrationCompleteEvent(registered, request.getLocale(), appUrl)); } catch (UserAlreadyExistException uaeEx) { ModelAndView mav = new ModelAndView("registration", "user", userDto); mav.addObject("message", "An account for that username/email already exists."); return mav; } catch (RuntimeException ex) { return new ModelAndView("emailError", "user", userDto); } return new ModelAndView("successRegister", "user", userDto); }

Une autre chose à noter est le bloc try catch entourant la publication de l'événement. Ce morceau de code affichera une page d'erreur à chaque fois qu'il y aura une exception dans la logique exécutée après la publication de l'événement, qui dans ce cas est l'envoi de l'email.

3.2. L'événement et l'auditeur

Voyons maintenant l'implémentation réelle de ce nouvel OnRegistrationCompleteEvent que notre contrôleur envoie, ainsi que l'auditeur qui va le gérer:

Exemple 3.2.1. - L' événement OnRegistrationCompleteEvent

public class OnRegistrationCompleteEvent extends ApplicationEvent { private String appUrl; private Locale locale; private User user; public OnRegistrationCompleteEvent( User user, Locale locale, String appUrl) { super(user); this.user = user; this.locale = locale; this.appUrl = appUrl; } // standard getters and setters }

Exemple 3.2.2. - Le RegistrationListener gère l' événement OnRegistrationCompleteEvent

@Component public class RegistrationListener implements ApplicationListener { @Autowired private IUserService service; @Autowired private MessageSource messages; @Autowired private JavaMailSender mailSender; @Override public void onApplicationEvent(OnRegistrationCompleteEvent event) { this.confirmRegistration(event); } private void confirmRegistration(OnRegistrationCompleteEvent event) { User user = event.getUser(); String token = UUID.randomUUID().toString(); service.createVerificationToken(user, token); String recipientAddress = user.getEmail(); String subject = "Registration Confirmation"; String confirmationUrl = event.getAppUrl() + "/regitrationConfirm.html?token=" + token; String message = messages.getMessage("message.regSucc", null, event.getLocale()); SimpleMailMessage email = new SimpleMailMessage(); email.setTo(recipientAddress); email.setSubject(subject); email.setText(message + "\r\n" + "//localhost:8080" + confirmationUrl); mailSender.send(email); } }

Ici, la méthode confirmRegistration recevra l' OnRegistrationCompleteEvent , en extraira toutes les informations utilisateur nécessaires , créera le jeton de vérification, le conservera, puis l'envoyer en tant que paramètre dans le lien « Confirmer l'enregistrement ».

Comme mentionné ci-dessus, toute exception javax.mail.AuthenticationFailedException lancée par JavaMailSender sera gérée par le contrôleur.

3.3. Traitement du paramètre de jeton de vérification

Lorsque l'utilisateur reçoit le lien « Confirmer l'inscription », il doit cliquer dessus.

Une fois qu'ils le font, le contrôleur extraira la valeur du paramètre de jeton dans la requête GET résultante et l'utilisera pour activer l' utilisateur .

Voyons ce processus dans l'exemple 3.3.1:

Exemple 3.3.1. - RegistrationController traitant la confirmation d'inscription

@Autowired private IUserService service; @GetMapping("/regitrationConfirm") public String confirmRegistration (WebRequest request, Model model, @RequestParam("token") String token) { Locale locale = request.getLocale(); VerificationToken verificationToken = service.getVerificationToken(token); if (verificationToken == null) { String message = messages.getMessage("auth.message.invalidToken", null, locale); model.addAttribute("message", message); return "redirect:/badUser.html?lang=" + locale.getLanguage(); } User user = verificationToken.getUser(); Calendar cal = Calendar.getInstance(); if ((verificationToken.getExpiryDate().getTime() - cal.getTime().getTime()) <= 0) { String messageValue = messages.getMessage("auth.message.expired", null, locale) model.addAttribute("message", messageValue); return "redirect:/badUser.html?lang=" + locale.getLanguage(); } user.setEnabled(true); service.saveRegisteredUser(user); return "redirect:/login.html?lang=" + request.getLocale().getLanguage(); }

L'utilisateur sera redirigé vers une page d'erreur avec le message correspondant si:

  1. Le VerificationToken n'existe pas, pour une raison quelconque ou
  2. Le VerificationToken a expiré

Voir l'exemple 3.3.2. pour voir la page d'erreur.

Exemple 3.3.2. - Le badUser.html

As we can see, now MyUserDetailsService not uses the enabled flag of the user – and so it will only allow enabled the user to authenticate.

Now, we will add an AuthenticationFailureHandler to customize the exception messages coming from MyUserDetailsService. Our CustomAuthenticationFailureHandler is shown in Example 4.2.:

Example 4.2. – CustomAuthenticationFailureHandler:

@Component public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Autowired private MessageSource messages; @Autowired private LocaleResolver localeResolver; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { setDefaultFailureUrl("/login.html?error=true"); super.onAuthenticationFailure(request, response, exception); Locale locale = localeResolver.resolveLocale(request); String errorMessage = messages.getMessage("message.badCredentials", null, locale); if (exception.getMessage().equalsIgnoreCase("User is disabled")) { errorMessage = messages.getMessage("auth.message.disabled", null, locale); } else if (exception.getMessage().equalsIgnoreCase("User account has expired")) { errorMessage = messages.getMessage("auth.message.expired", null, locale); } request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, errorMessage); } }

We will need to modify login.html to show the error messages.

Example 4.3. – Display error messages at login.html:

 error 

5. Adapting the Persistence Layer

Let's now provide the actual implementation of some of these operations involving the verification token as well as the users.

We'll cover:

  1. A new VerificationTokenRepository
  2. New methods in the IUserInterface and its implementation for new CRUD operations needed

Examples 5.1 – 5.3. show the new interfaces and implementation:

Example 5.1. – The VerificationTokenRepository

public interface VerificationTokenRepository extends JpaRepository { VerificationToken findByToken(String token); VerificationToken findByUser(User user); }

Example 5.2. – The IUserService Interface

public interface IUserService { User registerNewUserAccount(UserDto userDto) throws UserAlreadyExistException; User getUser(String verificationToken); void saveRegisteredUser(User user); void createVerificationToken(User user, String token); VerificationToken getVerificationToken(String VerificationToken); }

Example 5.3. The UserService

@Service @Transactional public class UserService implements IUserService { @Autowired private UserRepository repository; @Autowired private VerificationTokenRepository tokenRepository; @Override public User registerNewUserAccount(UserDto userDto) throws UserAlreadyExistException { if (emailExist(userDto.getEmail())) { throw new UserAlreadyExistException( "There is an account with that email adress: " + userDto.getEmail()); } User user = new User(); user.setFirstName(userDto.getFirstName()); user.setLastName(userDto.getLastName()); user.setPassword(userDto.getPassword()); user.setEmail(userDto.getEmail()); user.setRole(new Role(Integer.valueOf(1), user)); return repository.save(user); } private boolean emailExist(String email) { return userRepository.findByEmail(email) != null; } @Override public User getUser(String verificationToken) { User user = tokenRepository.findByToken(verificationToken).getUser(); return user; } @Override public VerificationToken getVerificationToken(String VerificationToken) { return tokenRepository.findByToken(VerificationToken); } @Override public void saveRegisteredUser(User user) { repository.save(user); } @Override public void createVerificationToken(User user, String token) { VerificationToken myToken = new VerificationToken(token, user); tokenRepository.save(myToken); } }

6. Conclusion

In this article, we've expanded the registration process to include an email based account activation procedure.

The account activation logic requires sending a verification token to the user via email so that they can send it back to the controller to verify their identity.

The implementation of this Registration with Spring Security tutorial can be found in the GitHub project – this is an Eclipse based project, so it should be easy to import and run as it is.

Next » Spring Security Registration – Resend Verification Email « Previous The Registration Process With Spring Security