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:
-
Ele deve se conectar de volta aoUser (por meio de uma relação unidirecional)
-
Será criado logo após o registro
-
Seráexpire within 24 hours após sua criação
-
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:
-
Gere oVerificationToken para o usuário e mantenha-o
-
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:
-
OVerificationToken não existe, por algum motivo ou
-
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:
-
We can use a Cron Job para verificar a expiração do token em segundo plano
-
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:
-
Um novoVerificationTokenRepository
-
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á.