Registrierung - Aktivieren Sie ein neues Konto per E-Mail

Registrierung - Aktivieren Sie ein neues Konto per E-Mail

1. Überblick

Dieser Artikel setzt dieongoing Registration with Spring Security series mit einem der fehlenden Teile des Registrierungsprozesses fort -verifying the user’s email to confirm their account.

Der Registrierungsbestätigungsmechanismus zwingt den Benutzer, auf eine nach erfolgreicher Registrierung gesendete E-Mail "Confirm Registration" 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, mit dem ein Benutzer verifiziert wird.

2.1. DieVerificationToken-Entität

Die EntitätVerificationTokenmuss die folgenden Kriterien erfüllen:

  1. Es muss eine Verknüpfung zuUser herstellen (über eine unidirektionale Beziehung).

  2. Es wird direkt nach der Registrierung erstellt

  3. Es wirdexpire within 24 hours nach seiner Erstellung

  4. Hat einenunique, randomly generated Wert

Die Anforderungen 2 und 3 sind Teil der Registrierungslogik. Die anderen beiden sind in einer einfachenVerificationToken-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 dienullable = false auf dem Benutzer, um die Datenintegrität und -konsistenz in derVerificationToken< → _User_-Zuordnung sicherzustellen.

2.2. Fügen Sie das Feldenabled zuUser hinzu

WennUser registriert ist, wird dieses Feldenabled zunächst auffalse gesetzt. Während des Kontobestätigungsprozesses wird es - falls erfolgreich - zutrue.

Beginnen wir mit dem Hinzufügen des Feldes zu unsererUser-Entitä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 auffalse setzen.

3. Während der Kontoregistrierung

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

  1. Generieren Sie dieVerificationToken für den Benutzer und behalten Sie sie bei

  2. Senden Sie die E-Mail-Nachricht zur Kontobestätigung, die einen Bestätigungslink mit dem WertVerificationToken’senthä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, bei denen es sich um Sicherheiten handelt.

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

Beispiel 3.1. zeigt diese einfache Logik:

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

Eine weitere zu beachtende Sache ist der Blocktry catch, der die Veröffentlichung des Ereignisses umgibt. Dieser Code zeigt eine Fehlerseite an, wenn in der nach der Veröffentlichung des Ereignisses ausgeführten Logik eine Ausnahme auftritt, in diesem Fall das Senden der E-Mail.

3.2. Das Ereignis und der Zuhörer

Sehen wir uns nun die tatsächliche Implementierung dieser neuenOnRegistrationCompleteEventan, die unser Controller sendet, sowie den Listener, der damit umgehen wird:

Example 3.2.1. - DieOnRegistrationCompleteEvent

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
}

Example 3.2.2. -The RegistrationListener BehandeltOnRegistrationCompleteEvent

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

Hier empfängt dieconfirmRegistration-Methode dieOnRegistrationCompleteEvent, extrahiert alle erforderlichenUser-Informationen daraus, erstellt das Verifikationstoken, behält es bei und sendet es dann als Parameter in „Confirm Registration ”Link.

Wie oben erwähnt, werden allejavax.mail.AuthenticationFailedException, die vonJavaMailSender ausgelöst werden, von der Steuerung verarbeitet.

3.3. Verarbeiten des Verifizierungs-Token-Parameters

Wenn der Benutzer den Link "Confirm Registration" erhält, sollte er darauf klicken.

Sobald dies der Fall ist, extrahiert der Controller den Wert des Token-Parameters in der resultierenden GET-Anforderung und aktiviert damitUser.

Sehen wir uns diesen Prozess in Beispiel 3.3.1 an:

Beispiel 3.3.1. -RegistrationController Verarbeitung der Registrierungsbestätigung

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

Der Benutzer wird auf eine Fehlerseite mit der entsprechenden Meldung weitergeleitet, wenn:

  1. DasVerificationToken existiert aus irgendeinem Grund nicht oder

  2. VerificationToken ist abgelaufen

See Example 3.3.2., um die Fehlerseite anzuzeigen.

Beispiel 3.3.2. - DiebadUser.html



    

signup

Wenn keine Fehler gefunden werden, wird der Benutzer aktiviert.

Es gibt zwei Möglichkeiten zur Verbesserung des Umgangs mit den Überprüfungs- und Ablaufszenarien vonVerificationToken:

  1. We can use a Cron Job, um im Hintergrund auf Token-Ablauf zu prüfen

  2. Wir könnengive the user the opportunity to get a new token, sobald es abgelaufen ist

Wir werden die Generierung eines neuen Tokens für einen zukünftigen Artikel verschieben und davon ausgehen, dass der Benutzer sein Token hier tatsächlich erfolgreich überprüft.

4. Hinzufügen der Kontoaktivierungsprüfung zum Anmeldevorgang

Wir müssen den Code hinzufügen, der überprüft, ob der Benutzer aktiviert ist:

Sehen wir uns das in Beispiel 4.1 an. Dies zeigt dieloadUserByUsername-Methode vonMyUserDetailsService.

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

Wie wir sehen können, verwendetMyUserDetailsService jetzt nicht dasenabled-Flag des Benutzers - und ermöglicht daher nur die Aktivierung des Benutzers zur Authentifizierung.

Jetzt fügen wirAuthenticationFailureHandler hinzu, um die vonMyUserDetailsService kommenden Ausnahmemeldungen anzupassen. UnsereCustomAuthenticationFailureHandler sind in Beispiel 4.2 gezeigt.:

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

Wir müssenlogin.html ändern, um die Fehlermeldungen anzuzeigen.

Beispiel 4.3. - Fehlermeldungen beilogin.html anzeigen:

error

5. Anpassen der Persistenzschicht

Lassen Sie uns nun die tatsächliche Implementierung einiger dieser Vorgänge bereitstellen, an denen sowohl das Verifizierungstoken als auch die Benutzer beteiligt sind.

Wir decken ab:

  1. Ein neuesVerificationTokenRepository

  2. Neue Methoden inIUserInterface und deren Implementierung für neue CRUD-Operationen erforderlich

Beispiele 5.1 - 5.3. Zeigen Sie die neuen Schnittstellen und die Implementierung:

Example 5.1. - DieVerificationTokenRepository

public interface VerificationTokenRepository
  extends JpaRepository {

    VerificationToken findByToken(String token);

    VerificationToken findByUser(User user);
}

Example 5.2. - DieIUserService-Schnittstelle

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

Example 5.3. DieUserService

@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. Fazit

In diesem Artikel haben wir den Registrierungsprozess uman email based account activation procedure erweitert.

Für die Kontoaktivierungslogik muss dem Benutzer ein Bestätigungstoken per E-Mail gesendet werden, damit er es zur Überprüfung seiner Identität an den Controller zurücksenden kann.

Die Implementierung dieses Lernprogramms zur Registrierung bei Spring Security finden Sie inthe GitHub project - dies ist ein Eclipse-basiertes Projekt, daher sollte es einfach zu importieren und auszuführen sein.