O processo de registro com Spring Security

O processo de registro com Spring Security

1. Visão geral

Neste artigo, implementaremos um processo de registro básico com Spring Security. Isso se baseia nos conceitos explorados emprevious article, onde examinamos o login.

O objetivo aqui é adicionara full registration process que permite que um usuário se inscreva, valide e persista os dados do usuário.

Leitura adicional:

Suporte assíncrono do Servlet 3 com Spring MVC e Spring Security

Introdução rápida ao suporte do Spring Security para solicitações assíncronas no Spring MVC.

Read more

Segurança de primavera com Thymeleaf

Um guia rápido para integrar Spring Security e Thymeleaf

Read more

Spring Security - Cabeçalhos de controle de cache

Um guia para controlar cabeçalhos de controle de cache HTTP com o Spring Security.

Read more

2. A página de registro

Primeiro - vamos implementar uma página de registro simples exibindothe following fields:

  • name (nome e sobrenome)

  • o email

  • password (e campo de confirmação de senha)

O exemplo a seguir mostra uma páginaregistration.html simples:

Exemplo 2.1



form

Validation error

Validation error

Validation error

Validation error

login

3. O objeto DTO do usuário

Precisamos de umData Transfer Object para enviar todas as informações de registro para nosso back-end Spring. O objetoDTO deve ter todas as informações que precisaremos mais tarde, quando criarmos e preenchermos nosso objetoUser:

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
}

Observe que usamos anotaçõesjavax.validation padrão nos campos do objeto DTO. Mais tarde, iremos tambémimplement our own custom validation annotations para validar o formato do endereço de email, bem como para a confirmação da senha. (vejaSection 5)

4. O controlador de registro

Um linkSign-Up na páginalogin levará o usuário à páginaregistration. Este back-end para essa página reside no controlador de registro e é mapeado para“/user/registration”:

Exemplo 4.1 - O MétodoshowRegistration

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

Quando o controlador recebe a solicitação“/user/registration”, ele cria o novo objetoUserDto que fará o backup do formulárioregistration, vincula-o e retorna - bem direto.

5. Validando Dados de Registro

A seguir - vamos ver as validações que o controlador irá realizar ao registrar uma nova conta:

  1. Todos os campos obrigatórios são preenchidos (sem campos vazios ou nulos)

  2. O endereço de email é válido (bem formado)

  3. O campo de confirmação da senha corresponde ao campo da senha

  4. A conta ainda não existe

5.1. A Validação Integrada

Para as verificações simples, usaremos as anotações de validação do bean de caixa no objeto DTO - anotações como@NotNull,@NotEmpty, etc.

Para acionar o processo de validação, vamos simplesmente anotar o objeto na camada do controlador com a anotação@Valid:

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

5.2. Validação personalizada para verificar a validade do email

Em seguida, vamos validar o endereço de e-mail e verificar se ele está bem formado. Vamos construir umcustom validator para isso, bem como umcustom validation annotation - vamos chamá-lo de@ValidEmail.

Uma nota rápida aqui - estamos lançando nossa própria anotação personalizadainstead of Hibernate’s@Email porque o Hibernate considera o formato de endereços de intranet antigo:[email protected] como válido (veja o artigoStackoverflow), que não é bom.

Aqui está a anotação de validação de e-mail e o validador personalizado:

Exemplo 5.2.1. - A anotação personalizada para validação de email

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

Observe que definimos a anotação no nívelFIELD - já que é onde ela se aplica conceitualmente.

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

Vamos agorause the new annotation em nossa implementaçãoUserDto:

@ValidEmail
@NotNull
@NotEmpty
private String email;

5.3. Usando validação personalizada para confirmação de senha

Também precisamos de uma anotação e validador personalizados para garantir que os campospassword ematchingPassword correspondam:

Exemplo 5.3.1. - A anotação personalizada para validação de confirmação de senha

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

Observe que a anotação@Target indica que esta é uma anotação de nívelTYPE. Isso ocorre porque precisamos de todo o objetoUserDto para realizar a validação.

O validador personalizado que será chamado por esta anotação é mostrado abaixo:

Exemplo 5.3.2. O validador personalizadoPasswordMatchesValidator

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

Agora, a anotação@PasswordMatches deve ser aplicada ao nosso objetoUserDto:

@PasswordMatches
public class UserDto {
   ...
}

É claro que todas as validações personalizadas são avaliadas juntamente com todas as anotações padrão quando todo o processo de validação é executado.

5.4. Verifique se a conta ainda não existe

A quarta verificação que implementaremos é verificar se a contaemail ainda não existe no banco de dados.

Isso é realizado depois que o formulário foi validado e é feito com a ajuda da implementaçãoUserService.

Exemplo 5.4.1. - O métodocreateUserAccount do controlador chama oUserService Object

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

Exemplo 5.4.2. -UserService verifica se há emails duplicados

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

O UserService depende da classeUserRepository para verificar se um usuário com um determinado endereço de e-mail já existe no banco de dados.

Agora - a implementação real deUserRepository na camada de persistência não é relevante para o artigo atual. Uma maneira rápida é, obviamente, parause Spring Data to generate the repository layer. __

6. Dados persistentes e processamento de formulário de finalização

Finalmente - vamos implementar a lógica de registro em nossa camada de controlador: __

Exemplo 6.1.1. - O métodoRegisterAccount no controlador

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

Pontos a serem observados no código acima:

  1. O controlador está retornando um objetoModelAndView que é a classe conveniente para enviar dados do modelo (user) vinculados à visualização.

  2. O controlador redirecionará para o formulário de registro se houver algum erro definido no momento da validação.

  3. O métodocreateUserAccount chamaUserService para persistência de dados. Discutiremos a implementação deUserService na seção a seguir

7. OUserService - Operação de registro

Vamos finalizar a implementação da operação de registro noUserService:

Exemplo 7.1 A interfaceIUserService

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

Exemplo 7.2. - A 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. Carregando detalhes do usuário para login de segurança

Em nossoprevious article, o login estava usando credenciais codificadas. Vamos mudar isso euse the newly registered user informatione credenciais. Implementaremos umUserDetailsService personalizado para verificar as credenciais de login da camada de persistência.

8.1. OUserDetailsService personalizado

Vamos começar com a implementação do serviço personalizado de detalhes do usuário:

@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. Habilite o novo provedor de autenticação

Para habilitar o novo serviço de usuário na configuração Spring Security - simplesmente precisamos adicionar uma referência aoUserDetailsService dentro do elementoauthentication-manager e adicionar o beanUserDetailsService:

Exemplo 8.2.- O gerenciador de autenticação e oUserDetailsService


    


Ou, via configuração Java:

@Autowired
private MyUserDetailsService userDetailsService;

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

9. Conclusão

E pronto - um completo e quaseproduction ready registration process implementado com Spring Security e Spring MVC. A seguir, discutiremos o processo de ativação da conta recém-registrada, verificando o e-mail do novo usuário.

A implementação deste tutorial REST do Spring Security pode ser encontrada emthe GitHub project - este é um projeto baseado em Eclipse, portanto, deve ser fácil de importar e executar como está.