Le processus d’inscription avec Spring Security

Le processus d'inscription avec Spring Security

1. Vue d'ensemble

Dans cet article, nous allons mettre en œuvre un processus d'inscription de base avec Spring Security. Ceci s'appuie sur les concepts explorés dans lesprevious article, où nous avons examiné la connexion.

L'objectif ici est d'ajoutera full registration process qui permet à un utilisateur de s'inscrire, de valider et de conserver les données utilisateur.

Lectures complémentaires:

Prise en charge asynchrone de Servlet 3 avec Spring MVC et Spring Security

Introduction rapide à la prise en charge de Spring Security pour les demandes asynchrones dans Spring MVC.

Read more

Sécurité de printemps avec thymeleaf

Guide rapide d'intégration de Spring Security et de Thymeleaf

Read more

Spring Security - En-têtes de contrôle du cache

Guide de contrôle des en-têtes de contrôle de cache HTTP avec Spring Security.

Read more

2. La page d'inscription

Tout d'abord, implémentons une simple page d'inscription affichantthe following fields:

  • name (prénom et nom)

  • email

  • password (et champ de confirmation du mot de passe)

L'exemple suivant montre une simple pageregistration.html:

Exemple 2.1.



form

Validation error

Validation error

Validation error

Validation error

login

3. L'objet DTO utilisateur

Nous avons besoin d'unData Transfer Object pour envoyer toutes les informations d'inscription à notre backend Spring. L'objetDTO doit contenir toutes les informations dont nous aurons besoin plus tard lorsque nous créerons et remplirons notre objetUser:

public class UserDto {
    @NotNull
    @NotEmpty
    private String firstName;

    @NotNull
    @NotEmpty
    private String lastName;

    @NotNull
    @NotEmpty
    private String password;
    private String matchingPassword;

    @NotNull
    @NotEmpty
    private String email;

    // standard getters and setters
}

Remarquez que nous avons utilisé des annotations standardjavax.validation sur les champs de l'objet DTO. Plus tard, nous irons également àimplement our own custom validation annotations pour valider le format de l'adresse e-mail ainsi que pour la confirmation du mot de passe. (voirSection 5)

4. Le contrôleur d'enregistrement

Un lienSign-Up sur la pagelogin amènera l'utilisateur à la pageregistration. Ce back-end pour cette page vit dans le contrôleur d'enregistrement et est mappé à“/user/registration”:

Exemple 4.1. - La méthodeshowRegistration

@RequestMapping(value = "/user/registration", method = RequestMethod.GET)
public String showRegistrationForm(WebRequest request, Model model) {
    UserDto userDto = new UserDto();
    model.addAttribute("user", userDto);
    return "registration";
}

Lorsque le contrôleur reçoit la requête“/user/registration”, il crée le nouvel objetUserDto qui sauvegardera le formulaireregistration, le lie et retourne - assez simple.

5. Validation des données d'inscription

Ensuite, examinons les validations que le contrôleur effectuera lors de l'enregistrement d'un nouveau compte:

  1. Tous les champs obligatoires sont remplis (pas de champs vides ou nuls)

  2. L'adresse email est valide (bien formé)

  3. Le champ de confirmation du mot de passe correspond au champ du mot de passe

  4. Le compte n'existe pas déjà

5.1. La validation intégrée

Pour les vérifications simples, nous utiliserons les annotations de validation de bean prêtes à l'emploi sur l'objet DTO - des annotations telles que@NotNull,@NotEmpty, etc.

Pour déclencher le processus de validation, nous allons simplement annoter l'objet dans la couche de contrôleur avec l'annotation@Valid:

public ModelAndView registerUserAccount(
  @ModelAttribute("user") @Valid UserDto accountDto,
  BindingResult result, WebRequest request, Errors errors) {
    ...
}

5.2. Validation personnalisée pour vérifier la validité des e-mails

Ensuite, validons l’adresse e-mail et vérifions qu’elle est bien formée. Nous allons construire uncustom validator pour cela, ainsi qu'uncustom validation annotation - appelons cela@ValidEmail.

Une petite remarque ici - nous lançons notre propre annotation personnaliséeinstead of Hibernate’s@Email car Hibernate considère l'ancien format des adresses intranet:[email protected] comme valide (voir l'articleStackoverflow), qui n'est pas bon.

Voici l'annotation de validation par e-mail et le validateur personnalisé:

Exemple 5.2.1. - L'annotation personnalisée pour la validation des e-mails

@Target({TYPE, FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = EmailValidator.class)
@Documented
public @interface ValidEmail {
    String message() default "Invalid email";
    Class[] groups() default {};
    Class[] payload() default {};
}

Notez que nous avons défini l'annotation au niveauFIELD - puisque c'est là qu'elle s'applique conceptuellement.

Example 5.2.2. – The Custom EmailValidator:

public class EmailValidator
  implements ConstraintValidator {

    private Pattern pattern;
    private Matcher matcher;
    private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-+]+
        (.[_A-Za-z0-9-]+)*@" + "[A-Za-z0-9-]+(.[A-Za-z0-9]+)*
        (.[A-Za-z]{2,})$";
    @Override
    public void initialize(ValidEmail constraintAnnotation) {
    }
    @Override
    public boolean isValid(String email, ConstraintValidatorContext context){
        return (validateEmail(email));
    }
    private boolean validateEmail(String email) {
        pattern = Pattern.compile(EMAIL_PATTERN);
        matcher = pattern.matcher(email);
        return matcher.matches();
    }
}

Passons maintenant àuse the new annotation sur notre implémentation deUserDto:

@ValidEmail
@NotNull
@NotEmpty
private String email;

5.3. Utilisation de la validation personnalisée pour la confirmation du mot de passe

Nous avons également besoin d'une annotation et d'un validateur personnalisés pour nous assurer que les champspassword etmatchingPassword correspondent:

Exemple 5.3.1. - L'annotation personnalisée pour la validation de la confirmation du mot de passe

@Target({TYPE,ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = PasswordMatchesValidator.class)
@Documented
public @interface PasswordMatches {
    String message() default "Passwords don't match";
    Class[] groups() default {};
    Class[] payload() default {};
}

Notez que l'annotation@Target indique qu'il s'agit d'une annotation de niveauTYPE. C'est parce que nous avons besoin de tout l'objetUserDto pour effectuer la validation.

Le validateur personnalisé qui sera appelé par cette annotation est présenté ci-dessous:

Exemple 5.3.2. Le validateur personnaliséPasswordMatchesValidator

public class PasswordMatchesValidator
  implements ConstraintValidator {

    @Override
    public void initialize(PasswordMatches constraintAnnotation) {
    }
    @Override
    public boolean isValid(Object obj, ConstraintValidatorContext context){
        UserDto user = (UserDto) obj;
        return user.getPassword().equals(user.getMatchingPassword());
    }
}

Maintenant, l'annotation@PasswordMatches doit être appliquée à notre objetUserDto:

@PasswordMatches
public class UserDto {
   ...
}

Toutes les validations personnalisées sont bien sûr évaluées avec toutes les annotations standard lorsque l'ensemble du processus de validation est exécuté.

5.4. Vérifiez que le compte n'existe pas déjà

La quatrième vérification que nous allons mettre en œuvre consiste à vérifier que le compteemail n’existe pas déjà dans la base de données.

Ceci est effectué après que le formulaire a été validé et cela est fait à l’aide de l’implémentation deUserService.

Exemple 5.4.1. - La méthodecreateUserAccount du contrôleur appelle le sbjectUserService O

@RequestMapping(value = "/user/registration", method = RequestMethod.POST)
public ModelAndView registerUserAccount
      (@ModelAttribute("user") @Valid UserDto accountDto,
      BindingResult result, WebRequest request, Errors errors) {
    User registered = new User();
    if (!result.hasErrors()) {
        registered = createUserAccount(accountDto, result);
    }
    if (registered == null) {
        result.rejectValue("email", "message.regError");
    }
    // rest of the implementation
}
private User createUserAccount(UserDto accountDto, BindingResult result) {
    User registered = null;
    try {
        registered = service.registerNewUserAccount(accountDto);
    } catch (EmailExistsException e) {
        return null;
    }
    return registered;
}

Exemple 5.4.2. -UserService vérifie les e-mails en double

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

    @Transactional
    @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());
        }
        ...
        // the rest of the registration operation
    }
    private boolean emailExist(String email) {
        User user = repository.findByEmail(email);
        if (user != null) {
            return true;
        }
        return false;
    }
}

UserService s'appuie sur la classeUserRepository pour vérifier si un utilisateur avec une adresse e-mail donnée existe déjà dans la base de données.

Maintenant, l'implémentation réelle desUserRepository dans la couche de persistance n'est pas pertinente pour l'article actuel. Un moyen rapide est, bien sûr, deuse Spring Data to generate the repository layer. __

6. Traitement des données persistantes et des formulaires de finition

Enfin, implémentons la logique d'enregistrement dans notre couche de contrôleur: __

Exemple 6.1.1. - La méthodeRegisterAccount dans le contrôleur

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

    User registered = new User();
    if (!result.hasErrors()) {
        registered = createUserAccount(accountDto, result);
    }
    if (registered == null) {
        result.rejectValue("email", "message.regError");
    }
    if (result.hasErrors()) {
        return new ModelAndView("registration", "user", accountDto);
    }
    else {
        return new ModelAndView("successRegister", "user", accountDto);
    }
}
private User createUserAccount(UserDto accountDto, BindingResult result) {
    User registered = null;
    try {
        registered = service.registerNewUserAccount(accountDto);
    } catch (EmailExistsException e) {
        return null;
    }
    return registered;
}

Les choses à noter dans le code ci-dessus:

  1. Le contrôleur renvoie un objetModelAndView qui est la classe pratique pour envoyer des données de modèle (user) liées à la vue.

  2. Le contrôleur redirigera vers le formulaire d’inscription s’il existe des erreurs lors de la validation.

  3. La méthodecreateUserAccount appelle lesUserService pour la persistance des données. Nous discuterons de l'implémentation deUserService dans la section suivante

7. L'opération de registreUserService

Finissons l’implémentation de l’opération d’enregistrement dans lesUserService:

Exemple 7.1. L'interfaceIUserService

public interface IUserService {
    User registerNewUserAccount(UserDto accountDto)
      throws EmailExistsException;
}

Exemple 7.2. - La classeUserService

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

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

        if (emailExists(accountDto.getEmail())) {
            throw new EmailExistsException(
              "There is an account with that email address:  + accountDto.getEmail());
        }
        User user = new User();
        user.setFirstName(accountDto.getFirstName());
        user.setLastName(accountDto.getLastName());
        user.setPassword(accountDto.getPassword());
        user.setEmail(accountDto.getEmail());
        user.setRoles(Arrays.asList("ROLE_USER"));
        return repository.save(user);
    }
    private boolean emailExists(String email) {
        User user = repository.findByEmail(email);
        if (user != null) {
            return true;
        }
        return false;
    }
}

8. Chargement des détails de l'utilisateur pour la connexion de sécurité

Dans nosprevious article, la connexion utilisait des informations d'identification codées en dur. Modifions cela et lesuse the newly registered user information et les informations d'identification. Nous allons mettre en œuvre unUserDetailsService personnalisé pour vérifier les informations d'identification de connexion à partir de la couche de persistance.

8.1. LesUserDetailsService personnalisés

Commençons par la mise en œuvre du service de détails utilisateur personnalisés:

@Service
@Transactional
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;
    //
    public UserDetails loadUserByUsername(String email)
      throws UsernameNotFoundException {

        User user = userRepository.findByEmail(email);
        if (user == null) {
            throw new UsernameNotFoundException(
              "No user found with username: "+ email);
        }
        boolean enabled = true;
        boolean accountNonExpired = true;
        boolean credentialsNonExpired = true;
        boolean accountNonLocked = true;
        return  new org.springframework.security.core.userdetails.User
          (user.getEmail(),
          user.getPassword().toLowerCase(), enabled, accountNonExpired,
          credentialsNonExpired, accountNonLocked,
          getAuthorities(user.getRoles()));
    }

    private static List getAuthorities (List roles) {
        List authorities = new ArrayList<>();
        for (String role : roles) {
            authorities.add(new SimpleGrantedAuthority(role));
        }
        return authorities;
    }
}

8.2. Activer le nouveau fournisseur d'authentification

Pour activer le nouveau service utilisateur dans la configuration de Spring Security, nous devons simplement ajouter une référence auxUserDetailsService dans l'élémentauthentication-manager et ajouter le beanUserDetailsService:

Exemple 8.2.- Le gestionnaire d'authentification et lesUserDetailsService


    


Ou, via la configuration Java:

@Autowired
private MyUserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth)
  throws Exception {
    auth.userDetailsService(userDetailsService);
}

9. Conclusion

Et nous avons terminé - une mise en œuvre complète et presqueproduction ready registration processavec Spring Security et Spring MVC. Ensuite, nous allons discuter du processus d'activation du compte nouvellement enregistré en vérifiant l'adresse e-mail du nouvel utilisateur.

L'implémentation de ce tutoriel Spring Security REST se trouve dansthe GitHub project - il s'agit d'un projet basé sur Eclipse, il devrait donc être facile à importer et à exécuter tel quel.