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.
Sécurité de printemps avec thymeleaf
Guide rapide d'intégration de Spring Security et de Thymeleaf
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.
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
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:
-
Tous les champs obligatoires sont remplis (pas de champs vides ou nuls)
-
L'adresse email est valide (bien formé)
-
Le champ de confirmation du mot de passe correspond au champ du mot de passe
-
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 extends Payload>[] 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 extends Payload>[] 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:
-
Le contrôleur renvoie un objetModelAndView qui est la classe pratique pour envoyer des données de modèle (user) liées à la vue.
-
Le contrôleur redirigera vers le formulaire d’inscription s’il existe des erreurs lors de la validation.
-
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.