Registro - Ative uma nova conta por e-mail

Registro - Ative uma nova conta por e-mail

1. Visão geral

Este artigo continua oongoing Registration with Spring Security series com uma das partes ausentes do processo de registro -verifying the user’s email to confirm their account.

O mecanismo de confirmação de registro força o usuário a responder a um email “Confirm Registration” enviado após o registro com sucesso para verificar seu endereço de email e ativar sua conta. O usuário faz isso clicando em um link de ativação exclusivo enviado a eles por email.

Seguindo essa lógica, um usuário recém-registrado não poderá efetuar login no sistema até que esse processo seja concluído.

2. Um token de verificação

Usaremos um token de verificação simples como o artefato principal através do qual um usuário é verificado.

2.1. A EntidadeVerificationToken

A entidadeVerificationToken deve atender aos seguintes critérios:

  1. Ele deve se conectar de volta aoUser (por meio de uma relação unidirecional)

  2. Será criado logo após o registro

  3. Seráexpire within 24 hours após sua criação

  4. Tem um valorunique, randomly generated

Os requisitos 2 e 3 fazem parte da lógica de registro. Os outros dois são implementados em uma entidadeVerificationToken simples como a do Exemplo 2.1 .:

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

Observe onullable = false no usuário para garantir a integridade dos dados e consistência na associaçãoVerificationToken< → _User_.

2.2. Adicione o campoenabled aUser

Inicialmente, quando oUser for registrado, este campoenabled será definido comofalse. Durante o processo de verificação da conta - se bem-sucedido - ele se tornarátrue.

Vamos começar adicionando o campo à nossa entidadeUser:

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

    public User() {
        super();
        this.enabled=false;
    }
    ...
}

Observe como também definimos o valor padrão deste campo parafalse.

3. Durante o registro da conta

Vamos adicionar duas peças adicionais de lógica de negócios ao caso de uso de registro do usuário:

  1. Gere oVerificationToken para o usuário e mantenha-o

  2. Envie a mensagem de e-mail para confirmação da conta - que inclui um link de confirmação com o valorVerificationToken’s

3.1. Usando um evento Spring para criar o token e enviar o e-mail de verificação

Essas duas partes adicionais de lógica não devem ser executadas diretamente pelo controlador, porque são tarefas de back-end "colaterais".

O controlador publicará um SpringApplicationEvent para acionar a execução dessas tarefas. Isso é tão simples quanto injetarApplicationEventPublishere usá-lo para publicar a conclusão do registro.

Exemplo 3.1 mostra esta lógica simples:

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

Uma coisa adicional a se notar é o blocotry catch em torno da publicação do evento. Esse trecho de código exibirá uma página de erro sempre que houver uma exceção na lógica executada após a publicação do evento, que neste caso é o envio do email.

3.2. O evento e o ouvinte

Vamos agora ver a implementação real deste novoOnRegistrationCompleteEvent que nosso controlador está enviando, bem como o ouvinte que vai lidar com isso:

Example 3.2.1. - oOnRegistrationCompleteEvent

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 Lida comOnRegistrationCompleteEvent

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

Aqui, o métodoconfirmRegistration receberá oOnRegistrationCompleteEvent, extrairá todas as informaçõesUser necessárias, criará o token de verificação, persistirá e, em seguida, enviará como um parâmetro no campo “Confirm Registration ”link.

Como mencionado acima, qualquerjavax.mail.AuthenticationFailedException lançado porJavaMailSender será gerenciado pelo controlador.

3.3. Processando o parâmetro do token de verificação

Quando o usuário receber o link “Confirm Registration”, deve clicar nele.

Assim que o fizerem, o controlador extrairá o valor do parâmetro token na solicitação GET resultante e o usará para habilitarUser.

Vejamos esse processo no Exemplo 3.3.1 .:

Exemplo 3.3.1. -RegistrationController Processando a confirmação de registro

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

O usuário será redirecionado para uma página de erro com a mensagem correspondente se:

  1. OVerificationToken não existe, por algum motivo ou

  2. OVerificationToken expirou

See Example 3.3.2. para ver a página de erro.

Exemplo 3.3.2. - ObadUser.html



    

signup

Se nenhum erro for encontrado, o usuário está ativado.

Existem duas oportunidades de melhoria no tratamento dos cenários de verificação e expiração deVerificationToken:

  1. We can use a Cron Job para verificar a expiração do token em segundo plano

  2. Podemosgive the user the opportunity to get a new token uma vez que expirou

Adiaremos a geração de um novo token para um artigo futuro e presumiremos que o usuário de fato verifique com sucesso seu token aqui.

4. Adicionando verificação de ativação de conta ao processo de login

Precisamos adicionar o código que verificará se o usuário está habilitado:

Vamos ver isso no Exemplo 4.1. que mostra o métodoloadUserByUsername deMyUserDetailsService.

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

Como podemos ver, agoraMyUserDetailsService não usa o sinalizadorenabled do usuário - e assim só permitirá que o usuário se autentique.

Agora, adicionaremos umAuthenticationFailureHandler para personalizar as mensagens de exceção vindas deMyUserDetailsService. NossoCustomAuthenticationFailureHandler é mostrado no Exemplo 4.2.:

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

Precisaremos modificarlogin.html para mostrar as mensagens de erro.

Exemplo 4.3. - Exibir mensagens de erro emlogin.html:

error

5. Adaptando a Camada de Persistência

Vamos agora fornecer a implementação real de algumas dessas operações envolvendo o token de verificação, bem como os usuários.

Nós cobriremos:

  1. Um novoVerificationTokenRepository

  2. Novos métodos noIUserInterface e sua implementação para novas operações CRUD necessárias

Exemplos 5.1 - 5.3. mostre as novas interfaces e implementação:

Example 5.1. - oVerificationTokenRepository

public interface VerificationTokenRepository
  extends JpaRepository {

    VerificationToken findByToken(String token);

    VerificationToken findByUser(User user);
}

Example 5.2. - A interfaceIUserService

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. OUserService

@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. Conclusão

Neste artigo, expandimos o processo de registro para incluiran email based account activation procedure.

A lógica de ativação da conta requer o envio de um token de verificação ao usuário por email, para que ele possa enviá-lo de volta ao controlador para verificar sua identidade.

A implementação deste tutorial de Registro com 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á.