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:
-
Il doit renvoyer à l’utilisateur (via une relation unidirectionnelle)
-
Il sera créé juste après l’inscription
-
Il expirera dans les 24 heures suivant sa création
-
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:
-
Générer le VerificationToken pour l’utilisateur et le conserver
-
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:
-
Le VerificationToken n’existe pas, pour une raison quelconque ou
-
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 :
-
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:
-
Un nouveau VerificationTokenRepository
-
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