Enregistrement - Activer un nouveau compte par email

1. Vue d’ensemble

Cet article continue le lien:/spring-security-registration[en cours Enregistrement avec la série Spring Security ]avec l’un des éléments manquants du processus d’enregistrement - vérifiant le courrier électronique de l’utilisateur pour confirmer son compte .

Le mécanisme de confirmation d’inscription oblige 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. Pour ce faire, l’utilisateur clique sur un lien d’activation unique qui lui est envoyé par courrier électronique.

En suivant cette logique, un utilisateur nouvellement enregistré ne pourra pas se connecter au système tant que ce processus n’est pas terminé.

** 2. Un jeton de vérification

**

Nous utiliserons un simple jeton de vérification comme artefact clé permettant de vérifier un utilisateur.

** 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 une valeur unique, générée aléatoirement

Les exigences 2 et 3 font partie de la logique d’enregistrement. Les deux autres sont implémentés dans une simple entité VerificationToken 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 enabled à User

Initialement, lorsque l’utilisateur est enregistré, ce champ activé est défini sur __faux. Au cours du processus de vérification du compte, en cas de succès, il deviendra vrai.

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

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

    public User() {
        super();
        this.enabled=false;
    }
    ...
}

Notez que nous avons également défini la valeur par défaut de ce champ sur false .

3. Pendant l’enregistrement du compte

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

  1. Générer le VerificationToken pour l’utilisateur et le conserver

  2. Envoyez le courrier électronique pour la confirmation du compte - qui comprend

lien de confirmation avec la valeur VerificationToken’s

3.1. Utilisation d’un événement printanier pour créer le jeton et envoyer le courrier électronique de vérification

Ces deux éléments logiques supplémentaires ne doivent pas être exécutés directement par le contrôleur, car ils constituent des tâches principales «secondaires».

Le contrôleur publiera un Spring ApplicationEvent pour déclencher l’exécution de ces tâches. Ceci 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

@RequestMapping(value = "/user/registration", method = RequestMethod.POST)
public ModelAndView registerUserAccount(
  @ModelAttribute("user") @Valid UserDto accountDto,
  BindingResult result,
  WebRequest request,
  Errors errors) {

    if (result.hasErrors()) {
        return new ModelAndView("registration", "user", accountDto);
    }

    User registered = createUserAccount(accountDto);
    if (registered == null) {
        result.rejectValue("email", "message.regError");
    }
    try {
        String appUrl = request.getContextPath();
        eventPublisher.publishEvent(new OnRegistrationCompleteEvent
          (registered, request.getLocale(), appUrl));
    } catch (Exception me) {
        return new ModelAndView("emailError", "user", accountDto);
    }
    return new ModelAndView("successRegister", "user", accountDto);
}

Une chose supplémentaire à noter est le try catch block qui entoure la publication de l’événement. Ce morceau de code affichera une page d’erreur chaque fois qu’il y a une exception dans la logique exécutée après la publication de l’événement, qui est dans ce cas l’envoi du courrier électronique.

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

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

  • Exemple 3.2.1. ** - Le 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. - The RegistrationListener ** Gère l’événement OnRegistrationCompleteEvent

@Component
public class RegistrationListener implements
  ApplicationListener<OnRegistrationCompleteEvent> {

    @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 + " rn" + "http://localhost:8080" + confirmationUrl);
        mailSender.send(email);
    }
}

Ici, la méthode confirmRegistration recevra le OnRegistrationCompleteEvent , en extraira toutes les informations nécessaires de User , créera le jeton de vérification, le persistera, puis l’enverra en tant que paramètre dans le lien « Confirm Registration ».

Comme mentionné ci-dessus, toute javax.mail.AuthenticationFailedException levée par JavaMailSender sera traitée par le contrôleur.

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

Lorsque l’utilisateur reçoit le lien « Confirm Registration », il doit cliquer dessus.

Une fois cela fait, le contrôleur extraira la valeur du paramètre token dans la requête GET résultante et l’utilisera pour activer le User .

Voyons ce processus dans l’exemple 3.3.1:

  • Exemple 3.3.1. - RegistrationController Traitement de la confirmation d’inscription **

@Autowired
private IUserService service;

@RequestMapping(value = "/regitrationConfirm", method = RequestMethod.GET)
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 Exemple 3.3.2. ** Pour voir la page d’erreur.

    • Exemple 3.3.2. - Le badUser.html **

<html>
<body>
    <h1 th:text="${param.message[0]}>Error Message</h1>
    <a th:href="@{/registration.html}"
      th:text="#{label.form.loginSignUp}">signup</a>
</body>
</html>

Si aucune erreur n’est trouvée, l’utilisateur est activé.

Il existe deux possibilités d’amélioration dans la gestion des scénarios de vérification et d’expiration VerificationToken :

  1. Nous pouvons utiliser une tâche Cron pour vérifier l’expiration du jeton dans le

Contexte . Nous pouvons donner à l’utilisateur la possibilité d’obtenir un nouveau jeton une fois qu’il a

expiré

Nous reporterons la génération d’un nouveau jeton à un prochain article et supposerons que l’utilisateur vérifie effectivement leur jeton ici.

4. Ajout de la vérification d’activation de compte au processus de connexion

Nous devons ajouter le code qui vérifiera si l’utilisateur est activé:

Voyons cela dans l’exemple 4.1. qui montre la méthode loadUserByUsername de MyUserDetailsService .

  • Exemple 4.1. **

@Autowired
UserRepository userRepository;

public UserDetails loadUserByUsername(String email)
  throws UsernameNotFoundException {

    boolean enabled = true;
    boolean accountNonExpired = true;
    boolean credentialsNonExpired = true;
    boolean accountNonLocked = true;
    try {
        User user = userRepository.findByEmail(email);
        if (user == null) {
            throw new UsernameNotFoundException(
              "No user found with username: " + email);
        }

        return new org.springframework.security.core.userdetails.User(
          user.getEmail(),
          user.getPassword().toLowerCase(),
          user.isEnabled(),
          accountNonExpired,
          credentialsNonExpired,
          accountNonLocked,
          getAuthorities(user.getRole()));
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

Comme on peut le constater, maintenant, MyUserDetailsService n’utilise pas l’indicateur enabled de l’utilisateur - ce qui permet uniquement à l’utilisateur activé de s’authentifier.

Nous allons maintenant ajouter un AuthenticationFailureHandler pour personnaliser les messages d’exception provenant de MyUserDetailsService . Notre CustomAuthenticationFailureHandler est présenté dans l’exemple 4.2 . :

  • Exemple 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);
    }
}

Nous devrons modifier login.html pour afficher les messages d’erreur.

  • Exemple 4.3. - Afficher les messages d’erreur sur login.html : **

<div th:if="${param.error != null}"
  th:text="${session[SPRING__SECURITY__LAST__EXCEPTION]}">error</div>

5. Adapter la couche de persistance

Voyons maintenant l’implémentation réelle de certaines de ces opérations impliquant le jeton de vérification ainsi que les utilisateurs.

Nous couvrirons:

  1. Un nouveau VerificationTokenRepository

  2. Nouvelles méthodes dans IUserInterface et son implémentation pour les nouvelles

Opérations CRUD nécessaires

Exemples 5.1 - 5.3. montrer les nouvelles interfaces et implémentation:

  • Exemple 5.1. ** - Le VerificationTokenRepository

public interface VerificationTokenRepository
  extends JpaRepository<VerificationToken, Long> {

    VerificationToken findByToken(String token);

    VerificationToken findByUser(User user);
}
  • Exemple 5.2. ** - L’interface IUserService

public interface IUserService {

    User registerNewUserAccount(UserDto accountDto)
      throws EmailExistsException;

    User getUser(String verificationToken);

    void saveRegisteredUser(User user);

    void createVerificationToken(User user, String token);

    VerificationToken getVerificationToken(String VerificationToken);
}
  • Exemple 5.3. ** Le UserService

@Service
@Transactional
public class UserService implements IUserService {
    @Autowired
    private UserRepository repository;

    @Autowired
    private VerificationTokenRepository tokenRepository;

    @Override
    public User registerNewUserAccount(UserDto accountDto)
      throws EmailExistsException {

        if (emailExist(accountDto.getEmail())) {
            throw new EmailExistsException(
              "There is an account with that email adress: "
              + accountDto.getEmail());
        }

        User user = new User();
        user.setFirstName(accountDto.getFirstName());
        user.setLastName(accountDto.getLastName());
        user.setPassword(accountDto.getPassword());
        user.setEmail(accountDto.getEmail());
        user.setRole(new Role(Integer.valueOf(1), user));
        return repository.save(user);
    }

    private boolean emailExist(String email) {
        User user = repository.findByEmail(email);
        if (user != null) {
            return true;
        }
        return false;
    }

    @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

Dans cet article, nous avons étendu le processus d’enregistrement pour inclure une procédure d’activation de compte par courrier électronique .

La logique d’activation du compte nécessite l’envoi d’un jeton de vérification à l’utilisateur par courrier électronique afin qu’il puisse le renvoyer au contrôleur pour vérifier son identité.

Vous trouverez la mise en œuvre de ce didacticiel sur l’inscription avec Spring Security à l’adresse le projet GitHub . Il s’agit d’un projet basé sur Eclipse. tel quel.

Suivant "

  • "** Précédent