Der Registrierungsprozess mit Spring Security

Der Registrierungsprozess mit Frühlingssicherheit

1. Überblick

In diesem Artikel implementieren wir einen grundlegenden Registrierungsprozess bei Spring Security. Dies baut auf den Konzepten auf, die inprevious articleuntersucht wurden, wo wir uns die Anmeldung angesehen haben.

Ziel ist es,a full registration process hinzuzufügen, mit denen sich ein Benutzer anmelden, Benutzerdaten validieren und beibehalten kann.

Weitere Lektüre:

Servlet 3 Async Support mit Spring MVC und Spring Security

Schnelle Einführung in die Unterstützung von Spring Security für asynchrone Anforderungen in Spring MVC.

Read more

Frühlingssicherheit mit Thymeleaf

Eine Kurzanleitung zur Integration von Spring Security und Thymeleaf

Read more

Frühjahrssicherheit - Header der Cache-Steuerung

Eine Anleitung zum Steuern von HTTP-Cache-Steuerelement-Headern mit Spring Security.

Read more

2. Die Registrierungsseite

Zunächst implementieren wir eine einfache Registrierungsseite mitthe following fields:

  • name (Vor- und Nachname)

  • Email

  • password (und Passwortbestätigungsfeld)

Das folgende Beispiel zeigt eine einfacheregistration.html-Seite:

Beispiel 2.1.



form

Validation error

Validation error

Validation error

Validation error

login

3. Das Benutzer-DTO-Objekt

Wir benötigen einData Transfer Object, um alle Registrierungsinformationen an unser Spring-Backend zu senden. DasDTO-Objekt sollte alle Informationen enthalten, die wir später beim Erstellen und Auffüllen unseresUser-Objekts benötigen:

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
}

Beachten Sie, dass wir Standard-javax.validation-Anmerkungen für die Felder des DTO-Objekts verwendet haben. Später werden wir auchimplement our own custom validation annotations verwenden, um das Format der E-Mail-Adresse sowie die Kennwortbestätigung zu überprüfen. (sieheSection 5)

4. Der Registrierungscontroller

EinSign-Up-Link auf derlogin-Seite führt den Benutzer zurregistration-Seite. Dieses Backend für diese Seite befindet sich im Registrierungscontroller und ist“/user/registration” zugeordnet:

Beispiel 4.1. - DieshowRegistration-Methode

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

Wenn der Controller die Anforderung“/user/registration” empfängt, erstellt er das neueUserDto-Objekt, das dasregistration-Formular unterstützt, bindet und zurückgibt - ziemlich einfach.

5. Registrierungsdaten validieren

Weiter - Schauen wir uns die Überprüfungen an, die der Controller bei der Registrierung eines neuen Kontos durchführt:

  1. Alle Pflichtfelder sind ausgefüllt (keine leeren oder leeren Felder)

  2. Die E-Mail-Adresse ist gültig (wohlgeformt)

  3. Das Kennwortbestätigungsfeld stimmt mit dem Kennwortfeld überein

  4. Das Konto ist noch nicht vorhanden

5.1. Die integrierte Validierung

Für die einfachen Überprüfungen verwenden wir die sofort einsatzbereiten Bean-Validierungsanmerkungen für das DTO-Objekt - Anmerkungen wie@NotNull,@NotEmpty usw.

Um den Validierungsprozess auszulösen, kommentieren wir das Objekt in der Controller-Ebene einfach mit der Annotation@Valid:

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

5.2. Benutzerdefinierte Validierung zur Überprüfung der E-Mail-Gültigkeit

Weiter - Überprüfen Sie die E-Mail-Adresse und stellen Sie sicher, dass sie korrekt ist. Wir werden dafür eincustom validator sowie eincustom validation annotation erstellen - nennen wir das@ValidEmail.

Eine kurze Randnotiz hier - wir rollen unsere eigene benutzerdefinierte Annotationinstead of Hibernate’s@Email, da Hibernate das alte Format der Intranetadressen als gültig betrachtet:[email protected] (sieheStackoverflow Artikel) ist nicht gut.

Hier ist die Anmerkung zur E-Mail-Validierung und der benutzerdefinierte Validator:

Beispiel 5.2.1. - Die benutzerdefinierte Anmerkung für die E-Mail-Validierung

@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 {};
}

Beachten Sie, dass wir die Annotation auf der Ebene vonFIELDdefiniert haben - da sie dort konzeptionell gilt.

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

Lassen Sie uns nunuse the new annotation auf unsereUserDto-Implementierung setzen:

@ValidEmail
@NotNull
@NotEmpty
private String email;

5.3. Verwenden der benutzerdefinierten Validierung zur Kennwortbestätigung

Wir benötigen außerdem eine benutzerdefinierte Anmerkung und einen benutzerdefinierten Validator, um sicherzustellen, dass die Felderpassword undmatchingPasswordübereinstimmen:

Beispiel 5.3.1. - Die benutzerdefinierte Anmerkung für die Überprüfung der Passwortbestätigung

@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 {};
}

Beachten Sie, dass die Annotation@Targetangibt, dass es sich um eine Annotation der EbeneTYPEhandelt. Dies liegt daran, dass wir das gesamteUserDto-Objekt benötigen, um die Validierung durchzuführen.

Der benutzerdefinierte Validator, der von dieser Annotation aufgerufen wird, ist unten dargestellt:

Beispiel 5.3.2. Der benutzerdefinierte Validator vonPasswordMatchesValidator

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

Nun sollte die Annotation@PasswordMatchesauf das ObjektUserDtoangewendet werden:

@PasswordMatches
public class UserDto {
   ...
}

Alle benutzerdefinierten Überprüfungen werden natürlich zusammen mit allen Standardanmerkungen ausgewertet, wenn der gesamte Überprüfungsprozess ausgeführt wird.

5.4. Überprüfen Sie, ob das Konto noch nicht vorhanden ist

Die vierte Überprüfung, die wir durchführen, besteht darin, zu überprüfen, ob das Konto vonemailnoch nicht in der Datenbank vorhanden ist.

Dies erfolgt nach der Validierung des Formulars und mithilfe der Implementierung vonUserService.

Beispiel 5.4.1. - DiecreateUserAccount-Methode des Reglers ruft den Prozentsatz (t1)

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

Beispiel 5.4.2. -UserService Überprüft auf doppelte E-Mails

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

Der UserService stützt sich auf die KlasseUserRepository, um zu überprüfen, ob ein Benutzer mit einer bestimmten E-Mail-Adresse bereits in der Datenbank vorhanden ist.

Nun - die tatsächliche Implementierung vonUserRepository in der Persistenzschicht ist für den aktuellen Artikel nicht relevant. Ein schneller Weg ist natürlichuse Spring Data to generate the repository layer. __

6. Persistierende Daten und Fertigstellung der Formularverarbeitung

Zum Schluss implementieren wir die Registrierungslogik in unserer Controller-Schicht: __

Beispiel 6.1.1. - DieRegisterAccount Methode im Controller

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

Dinge, die im obigen Code zu beachten sind:

  1. Die Steuerung gibt einModelAndView-Objekt zurück, das die bequeme Klasse zum Senden von Modelldaten (user) ist, die an die Ansicht gebunden sind.

  2. Der Controller leitet Sie zum Registrierungsformular weiter, wenn zum Zeitpunkt der Validierung Fehler aufgetreten sind.

  3. Die MethodecreateUserAccount ruftUserService für die Datenpersistenz auf. Wir werden die Implementierung vonUserServiceim folgenden Abschnitt diskutieren

7. DieUserService - Registeroperation

Beenden wir die Implementierung der Registrierungsoperation inUserService:

Beispiel 7.1. DieIUserService-Schnittstelle

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

Beispiel 7.2. - DieUserService Klasse

@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. Laden von Benutzerdetails für die Sicherheitsanmeldung

In unserenprevious article wurden bei der Anmeldung fest codierte Anmeldeinformationen verwendet. Ändern wir dies unduse the newly registered user information und Anmeldeinformationen. Wir implementieren ein benutzerdefiniertesUserDetailsService, um die Anmeldeinformationen für die Anmeldung über die Persistenzschicht zu überprüfen.

8.1. Die benutzerdefiniertenUserDetailsService

Beginnen wir mit der Implementierung des benutzerdefinierten Benutzerdetailservices:

@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. Aktivieren Sie den neuen Authentifizierungsanbieter

Um den neuen Benutzerdienst in der Spring Security-Konfiguration zu aktivieren, müssen Sie lediglich einen Verweis aufUserDetailsService im Elementauthentication-manager und die BeanUserDetailsService hinzufügen:

Beispiel 8.2.- Der Authentifizierungsmanager und dieUserDetailsService


    


Oder über die Java-Konfiguration:

@Autowired
private MyUserDetailsService userDetailsService;

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

9. Fazit

Und wir sind fertig - eine vollständige und fastproduction ready registration process implementiert mit Spring Security und Spring MVC. Als Nächstes wird der Vorgang zum Aktivieren des neu registrierten Kontos erläutert, indem die E-Mail-Adresse des neuen Benutzers überprüft wird.

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