Registrierung - Aktivieren Sie ein neues Konto per E-Mail

Dieser Artikel ist Teil einer Reihe: • Spring Security Registration Tutorial

• Der Registrierungsprozess mit Spring Security

• Registrierung - Aktivieren Sie ein neues Konto per E-Mail (aktueller Artikel). • Spring Security-Registrierung - Senden Sie die Bestätigungs-E-Mail erneut

• Registrierung bei Spring Security - Passwortkodierung

• Die Registrierungs-API wird RESTful

• Spring Security - Setzen Sie Ihr Passwort zurück

• Registrierung - Passwortstärke und Regeln

• Aktualisieren Sie Ihr Passwort

1. Übersicht

Dieser Artikel setzt die laufende Registrierung mit Spring Security - Serie mit einem der fehlenden Teilen des Registrierungsprozesses - der Benutzer E - Mail - Überprüfung ihres Konto zu bestätigen .

Der Registrierungsbestätigungsmechanismus zwingt den Benutzer, auf eine nach erfolgreicher Registrierung gesendete E-Mail mit dem Titel " Registrierung bestätigen " zu antworten , um seine E-Mail-Adresse zu überprüfen und sein Konto zu aktivieren. Der Benutzer klickt dazu auf einen eindeutigen Aktivierungslink, der ihm per E-Mail gesendet wird.

Nach dieser Logik kann sich ein neu registrierter Benutzer erst dann beim System anmelden, wenn dieser Vorgang abgeschlossen ist.

2. Ein Verifikationstoken

Wir werden ein einfaches Verifikationstoken als Schlüsselartefakt verwenden, durch das ein Benutzer verifiziert wird.

2.1. Die VerificationToken- Entität

Die VerificationToken- Entität muss die folgenden Kriterien erfüllen:

  1. Es muss eine Verbindung zum Benutzer herstellen (über eine unidirektionale Beziehung).
  2. Es wird direkt nach der Registrierung erstellt
  3. Es läuft innerhalb von 24 Stunden nach seiner Erstellung ab
  4. Hat einen eindeutigen, zufällig generierten Wert

Die Anforderungen 2 und 3 sind Teil der Registrierungslogik. Die anderen beiden sind in einer einfachen VerificationToken- Entität wie in Beispiel 2.1 implementiert :

Beispiel 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 }

Beachten Sie nullable = false für den Benutzer, um die Datenintegrität und -konsistenz in der VerificationToken < -> Benutzerzuordnung sicherzustellen .

2.2. Fügen Sie dem Benutzer das aktivierte Feld hinzu

Wenn der Benutzer registriert ist, wird dieses aktivierte Feld zunächst auf false gesetzt . Während des Kontobestätigungsprozesses wird es - falls erfolgreich - wahr .

Beginnen wir mit dem Hinzufügen des Felds zu unserer Benutzerentität :

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

Beachten Sie, wie wir auch den Standardwert dieses Feldes auf false setzen .

3. Während der Kontoregistrierung

Fügen wir dem Anwendungsfall für die Benutzerregistrierung zwei zusätzliche Geschäftslogiken hinzu:

  1. Generieren Sie das VerificationToken für den Benutzer und behalten Sie es bei
  2. Senden Sie die E-Mail-Nachricht zur Kontobestätigung, die einen Bestätigungslink mit dem Wert des VerificationToken enthält

3.1. Verwenden eines Frühlingsereignisses zum Erstellen des Tokens und Senden der Bestätigungs-E-Mail

Diese beiden zusätzlichen Logikelemente sollten nicht direkt vom Controller ausgeführt werden, da es sich um Back-End-Aufgaben handelt.

Der Controller veröffentlicht ein Spring ApplicationEvent , um die Ausführung dieser Aufgaben auszulösen. Dies ist so einfach wie das Injizieren des ApplicationEventPublisher und das anschließende Veröffentlichen des Registrierungsabschlusses.

Beispiel 3.1. zeigt diese einfache Logik:

Beispiel 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); }

Eine weitere zu beachtende Sache ist der Try-Catch- Block, der die Veröffentlichung des Ereignisses umgibt. Dieser Code zeigt eine Fehlerseite an, wenn eine Ausnahme in der Logik vorliegt, die nach der Veröffentlichung des Ereignisses ausgeführt wird. In diesem Fall handelt es sich um das Senden der E-Mail.

3.2. Das Ereignis und der Zuhörer

Sehen wir uns nun die tatsächliche Implementierung dieses neuen OnRegistrationCompleteEvent an , das unser Controller sendet, sowie den Listener, der damit umgehen wird:

Beispiel 3.2.1. - Das 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 }

Beispiel 3.2.2. - Der RegistrationListener behandelt das 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); } }

Hier empfängt die verifyRegistration- Methode das OnRegistrationCompleteEvent , extrahiert alle erforderlichen Benutzerinformationen daraus, erstellt das Bestätigungstoken, behält es bei und sendet es als Parameter im Link " Confirm Registration ".

As was mentioned above, any javax.mail.AuthenticationFailedException thrown by JavaMailSender will be handled by the controller.

3.3. Processing the Verification Token Parameter

When the user receives the “Confirm Registration” link they should click on it.

Once they do – the controller will extract the value of the token parameter in the resulting GET request and will use it to enable the User.

Let's see this process in Example 3.3.1.:

Example 3.3.1. – RegistrationController Processing the Registration Confirmation

@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(); }

The user will be redirected to an error page with the corresponding message if:

  1. The VerificationToken does not exist, for some reason or
  2. The VerificationToken has expired

See Example 3.3.2. to see the error page.

Example 3.3.2. – The 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