Spring boot Forgot password

Vamos lá! Como de costume vamos começar a partir do projeto criado nesse post onde temos autenticação feita com Spring security e JWT. 

Vamos criar o fluxo de recuperação de senha do usuário.

O objetivo é possibilitar que cada usuário possa resetar sua própria senha a partir de um link enviado por e-mail.

O quando o usuário acessa o link, informa a nova senha e a confirmação da nova senha para que a senha seja alterada.

Por questões de segurança, esse link deve ter um tempo de expiração e só poderá ser utilizado uma vez para evitar que outra pessoa consesiga alterar a senha utilizando o mesmo link.

Antes de começar vamos criar um atributo novo no arquivo de configuração para armazerar o tempo de expiração do token enviado no link.

package com.br.davifelipe.springjwt.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import lombok.Data;

@Configuration
@ConfigurationProperties(prefix = "auth")
@Data
public class AuthConfigProperties {
    
    private boolean publicSingUpUrlEnable;
    private String jwtSecret;
    private Long jwtExpirationMiliseg;
    private Long resetPasswordTokenExpirationMiliseg;
}

vamos replicar esse parâmetro novo em todos os arquivos properties. Também as configurações para envio de e-mail via SMTP. Você deve informar usuário e senha de uma conta gmail que o envio de e-mail funcione. Vou mostrar aqui somente o dev mas no repositório tem todos os arquivos de exemplo para usar como templete. 

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:file:~/test
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

application.smtp-sender=teste@example.com
application.reset-password-url=http://localhost:8080/auth/reset-password

spring.mail.host=smtp.gmail.com
spring.mail.username=
spring.mail.password=
spring.mail.properties.mail.smtp.auth = true
spring.mail.properties.mail.smtp.socketFactory.port = 465
spring.mail.properties.mail.smtp.socketFactory.class = javax.net.ssl.SSLSocketFactory
spring.mail.properties.mail.smtp.socketFactory.fallback = false
spring.mail.properties.mail.smtp.starttls.enable = true
spring.mail.properties.mail.smtp.ssl.enable = true

auth.public-sing-up-url-enable=true
auth.jwt-secret=MySecretJwtWord
auth.jwt-expiration-miliseg=86400000
auth.reset-password-token-expiration-miliseg=3600000

Adicionamos também outros dois novos parâmetros. Um pra informar o e-mail de envio e outro para informar o link para resetar a senha. Adicionamos esse link por que esse template não tem a camada front end. No texto do e-mail vamos instruir o usuário fazer uma requisição direto para o back end.

Mas quando a camada visual for implementada, o e-mail deve fornecer um link de um formulário (front end) para o usuário preencher a nova senha e só então enviar a requisição para o back end.

Dessa forma, quando o front end for implementado, esse link deve ser alterado para a página do font end.

Em security config vamos adicionar os endpoints reset-password e forgot-password como públicos

package com.br.davifelipe.springjwt.config;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import com.br.davifelipe.springjwt.security.JWTAuthenticationFilter;
import com.br.davifelipe.springjwt.security.JWTAuthorizationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    public static final List<String> PUBLIC_MATCHERS = new ArrayList<String>();
    public static final List<String> PUBLIC_MATCHERS_GET = new ArrayList<String>();
    public static final List<String> PUBLIC_MATCHERS_POST = new ArrayList<String>();
    
    @Autowired
    JWTUtil jwtUtil;
    
    @Autowired
    UserDetailsService userDetailsService;
    
    @Autowired
    private Environment env;
    
    @Value("${auth.public-sing-up-url-enable}")
    private String publicSingUpUrlEnable;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        
        PUBLIC_MATCHERS.add("/h2-console/**");
        PUBLIC_MATCHERS_GET.add("/product/**");
        PUBLIC_MATCHERS_POST.addAll(Arrays.asList(
                                        "/auth/reset-password/**",
                                        "/auth/forgot-password/**"
                                    ));
        
        String[] activeProfiles = env.getActiveProfiles();
        
        if (Arrays.asList(activeProfiles).contains("dev")
        || Arrays.asList(activeProfiles).contains("test")) {
            //disable it only for h2-console on dev envioment
            http.headers().frameOptions().disable();
        }
        
        if("true".equals(this.publicSingUpUrlEnable)) {
            PUBLIC_MATCHERS_POST.add("/auth/sing-up/**");
        }
        
        http.cors().and().csrf().disable();
        http.authorizeRequests()
            .antMatchers(PUBLIC_MATCHERS.toArray(new String[0])).permitAll()
            .antMatchers(HttpMethod.POST, PUBLIC_MATCHERS_POST.toArray(new String[0])).permitAll()
            .antMatchers(HttpMethod.GET, PUBLIC_MATCHERS_GET.toArray(new String[0])).permitAll()
            .anyRequest()
            .authenticated();
        http.addFilter(new JWTAuthenticationFilter(authenticationManager(), jwtUtil));
        http.addFilter(new JWTAuthorizationFilter(authenticationManager(), jwtUtil, userDetailsService));
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
    
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bcCryptPasswordEncoder());
    }
    
    @Bean
    CorsConfigurationSource configurationSource() {
        CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
        configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "DELETE", "OPTIONS"));
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
    
    @Bean
    public BCryptPasswordEncoder bcCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Vamos criar um novo model para armazerar o token do usuário com relacionamento one to one de token com usuário

package com.br.davifelipe.springjwt.model;

import java.io.Serializable;
import java.util.Calendar;
import java.util.Date;

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

@Entity
@Data
@ToString(exclude="id")
@EqualsAndHashCode(exclude={"token","createdDate"})
public class ResetPasswordToken implements Serializable{
    
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    private String token;
    
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;
    
    @OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
    @JoinColumn(nullable = false, name = "user_id")
    private User user;
    
    public boolean isExpired(Long resetPasswordTokenExpirationMisiseg) {
        return getTimeoutToken(resetPasswordTokenExpirationMisiseg).before(new Date());
    }
    
    public Calendar getTimeoutToken(Long resetPasswordTokenExpirationMisiseg) {
        Calendar timeout = Calendar.getInstance();
        timeout.setTimeInMillis(this.createdDate.getTime() + resetPasswordTokenExpirationMisiseg);
        return timeout;
    }
    
}

Em seguida o service e repository do token

package com.br.davifelipe.springjwt.services;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.br.davifelipe.springjwt.model.ResetPasswordToken;
import com.br.davifelipe.springjwt.repositories.ResetPasswordTokenRepository;
import com.br.davifelipe.springjwt.services.exceptions.ObjectNotFoundException;

@Service
public class ResetPasswordTokenService {
    
    @Autowired
    private ResetPasswordTokenRepository repo;
    
    /**
     * Find ResetPasswordToken by token
     * @param ResetPasswordToken id of the object
     * @return object found or null if the object were not found
     * */
    public ResetPasswordToken findByToken(String token) {
        Optional<ResetPasswordToken> obj = this.repo.findByToken(token);
        return obj.orElse(null);
    }
    
    /**
     * Find ResetPasswordToken by id
     * @param ResetPasswordToken id of the object
     * @return object found or null if the object were not found
     * */
    public ResetPasswordToken findById(Integer id) {
        Optional<ResetPasswordToken> obj = this.repo.findById(id);
        return obj.orElse(null);
    }
    
    /**
     * Insert a new resetPasswordToken
     * @param ResetPasswordToken resetPasswordToken to be inserted
     * @return ResetPasswordToken object inserted 
     * */
    public ResetPasswordToken insert(ResetPasswordToken obj) {
        obj.setId(null);
        return this.repo.save(obj);
    }
    
    /**
     * Update an resetPasswordToken
     * @param resetPasswordToken resetToken to be updated
     * @return resetPasswordToken object updated 
     * */
    public ResetPasswordToken update(ResetPasswordToken obj) {
        if(this.findById(obj.getId()) == null) {
            throw new ObjectNotFoundException("Obeject "+ResetPasswordToken.class.getName()+" no found! ID "+obj.getId());
        }
        return this.repo.save(obj);
    }
    
}

Repository

package com.br.davifelipe.springjwt.repositories;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import com.br.davifelipe.springjwt.model.ResetPasswordToken;

@Repository
public interface ResetPasswordTokenRepository extends JpaRepository<ResetPasswordToken, Integer>{
    
    @Transactional(readOnly = true)
    public Optional<ResetPasswordToken> findByToken(@Param("token") String token);
}

Agora precisamos implementar um endpoint para receber o e-mail do usuário e disparar um e-mail com o link para reiniciar a senha e outro endpoint para receber o token gerado e fazer de fato o update na senha. 

Mas para possibilitar que esse fluxo seja coberto por teste de integração temos que identificar quando a aplicação estiver sendo executada em ambiente de teste e apenas simular o envio de e-mail nessa situação. Dessa forma o envio de e-mail só vai acontecer de fato quando a aplicação não estiver rodando no ambiente de teste. 

Para resolver esse problema, vamos criar dias classes de configuração DevConfig e TestConfig. Vamos definir uma interface Emailservice com um método sendEmail. Cada classe de configuração vai entregar uma implementação diferente da interface de acordo com cada ambiente. 

No ambiente de teste a implementação do método sendEmail vai apenas simular um envio de e-mail mostrando o texto do email no log. E no ambiente de desenvolvimento envio de e-mail via SMTP será feito. Para o ambiente de produção essa devemos seguir essa mesma lógica.

 

package com.br.davifelipe.springjwt.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import com.br.davifelipe.springjwt.services.EmailService;
import com.br.davifelipe.springjwt.services.SmtpEmailService;

@Configuration
@Profile("dev")
public class DevConfig {

    @Bean
    public EmailService emailService() {
        return new SmtpEmailService();
    }
    
}

Test config

package com.br.davifelipe.springjwt.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import com.br.davifelipe.springjwt.services.EmailService;
import com.br.davifelipe.springjwt.services.MockEmailService;

@Configuration
@Profile("test")
public class TestConfig {
    
    @Bean
    public EmailService emailService() {
        return new MockEmailService();
    }
}

Antes de fazer a implementação de envio SMTP devemos adicionar as dependencias no pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

SmtpEmailService a implementação vai fazer o envio SMTP

package com.br.davifelipe.springjwt.services;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;

public class SmtpEmailService extends AbstractEmailService {
    
    @Autowired
    private MailSender mailSender;
    
    private static final Logger LOG = LoggerFactory.getLogger(SmtpEmailService.class);
    
    @Override
    public void sendEmail(SimpleMailMessage msg) {
        LOG.info("sending e-mail...");
        mailSender.send(msg);
        LOG.info("e-mail sent");

    }

}

A outra implementação que vai apenas simular o envio

package com.br.davifelipe.springjwt.services;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.mail.SimpleMailMessage;

public class MockEmailService extends AbstractEmailService {
    
    private static final Logger LOG = LoggerFactory.getLogger(MockEmailService.class);
    
    @Override
    public void sendEmail(SimpleMailMessage msg) {
        LOG.info("Emulating e-mail sending...");
        LOG.info(msg.toString());
        LOG.info("e-mail sent");
    }

}

Essas duas implementações são subclasses de AbstractEmailService

package com.br.davifelipe.springjwt.services;

import java.util.Date;

import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;

import com.br.davifelipe.springjwt.dto.ChangePasswordDTO;
import com.br.davifelipe.springjwt.model.ResetPasswordToken;
import com.br.davifelipe.springjwt.model.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public abstract class AbstractEmailService implements EmailService {
    
    @Value("${application.smtp-sender}")
    private String smtpSender;
    
    @Value("${application.reset-password-url}")
    private String resetPassowdUrl;
    
    public void sendResetPasswordToken(ResetPasswordToken resetPasswordToken) 
            throws JsonProcessingException {
        SimpleMailMessage msg = this.prepareResetPasswordEmail(resetPasswordToken);
        sendEmail(msg);
    }
    
    protected SimpleMailMessage prepareResetPasswordEmail(ResetPasswordToken resetPasswordToken) 
            throws JsonProcessingException{
        
        SimpleMailMessage sm = new SimpleMailMessage();
        User user = resetPasswordToken.getUser();
        sm.setTo(user.getEmail());
        sm.setFrom(smtpSender);
        sm.setSubject("Reset password");
        sm.setSentDate(new Date(System.currentTimeMillis()));
        
        ObjectMapper objectMapper = new ObjectMapper();
        ModelMapper modelMapper = new ModelMapper();
        ChangePasswordDTO dto = modelMapper.map(resetPasswordToken,ChangePasswordDTO.class);
        
        dto.setPassword("newPassword");
        dto.setPasswordConfirm("newPassword");
        
        StringBuilder sb = new StringBuilder();
        sb.append("I don't have a front end layer yet :( ");
        sb.append("Please access this URL [POST] to reset your password ");
        sb.append(resetPassowdUrl);
        sb.append(" body: ");
        sb.append(objectMapper.writeValueAsString(dto));
        
        sm.setText(sb.toString());
        return sm;
    }
    
}

Onde temos o método que vai montar o texto do e-mail. Esse método vai ser reaproveitado nas duas implementações. 

Observação: As subclasses não tem implements EmailService por que isso já está sendo feito em AbstractEmailService. Também é importante observar que em AbstractEmailService não temos a obrigação de implementar o método sendEmail por que essa obrigação desce apra as subclasses. 

Finalmente vamos implementar os resources dos endpoints forgot-password e reset-password

package com.br.davifelipe.springjwt.resources;

import java.net.URI;

import javax.validation.Valid;

import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import com.br.davifelipe.springjwt.dto.ChangePasswordDTO;
import com.br.davifelipe.springjwt.dto.ForgotPasswordDTO;
import com.br.davifelipe.springjwt.dto.MessageDTO;
import com.br.davifelipe.springjwt.dto.SingUpDTO;
import com.br.davifelipe.springjwt.model.ResetPasswordToken;
import com.br.davifelipe.springjwt.model.Role;
import com.br.davifelipe.springjwt.model.User;
import com.br.davifelipe.springjwt.services.EmailService;
import com.br.davifelipe.springjwt.services.ResetPasswordTokenService;
import com.br.davifelipe.springjwt.services.RoleService;
import com.br.davifelipe.springjwt.services.UserService;
import com.br.davifelipe.springjwt.services.exceptions.ObjectNotFoundException;
import com.fasterxml.jackson.core.JsonProcessingException;

@RestController
@RequestMapping("/auth")
public class AuthResource {
    
    @Autowired
    private UserService serviceUser;
    
    @Autowired
    private RoleService serviceRole;
    
    @Autowired
    private ResetPasswordTokenService serviceResetPassword;
    
    @Autowired
    EmailService emailService;
    
    @Value("${auth.reset-password-token-expiration-miliseg}")
    private Long resetPasswordTokenExpirationMisiseg;
    
    @PostMapping("/sing-up")
    public ResponseEntity<Void> singUp(@Valid @RequestBody SingUpDTO dto){
        
        ModelMapper modelMapper = new ModelMapper();
        User user = modelMapper.map(dto,User.class);
        
        Role roleUser = serviceRole.findOrInsertByName("ROLE_USER");
        
        user.addRole(roleUser);
        user = this.serviceUser.insert(user);
        
        URI uri = ServletUriComponentsBuilder
                  .fromCurrentContextPath().path("/{id}")
                  .buildAndExpand(user.getId())
                  .toUri();
        return ResponseEntity.created(uri).build();
    }
    
    @PostMapping("/reset-password")
    public ResponseEntity<?> findById(@Valid @RequestBody ChangePasswordDTO dto) {
        
        ResetPasswordToken resetPasswordToken = serviceResetPassword.findByToken(dto.getToken());
        if(resetPasswordToken == null) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST.value()).body(
                    new MessageDTO("Invalid link","Reset password error", HttpStatus.BAD_REQUEST.value())
                    );
        }
        
        if(resetPasswordToken.isExpired(resetPasswordTokenExpirationMisiseg)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST.value()).body(
                    new MessageDTO("Expired link","Reset password error", HttpStatus.BAD_REQUEST.value())
                    );
        }
        
        User user = resetPasswordToken.getUser();
        user.setPassword(dto.getPassword());
        serviceUser.update(user);
        //clean token to prevent that it will be used more than once
        resetPasswordToken.setToken(null);
        serviceResetPassword.update(resetPasswordToken);
        
        return ResponseEntity.ok().body(new MessageDTO("Password has been changed", HttpStatus.OK.value()));
    }
    
    @PostMapping("/forgot-password")
    public ResponseEntity<?> findById(@Valid @RequestBody ForgotPasswordDTO dto) 
            throws JsonProcessingException {
        
        User userFound = serviceUser.findByEmail(dto.getEmail());
        
        if(userFound == null) {
            throw new ObjectNotFoundException("Object "+User.class.getName()+" not found! e-mail "+dto.getEmail());
        }
        
        ResetPasswordToken resetPasswordToken = serviceUser.generateResetPasswordToken(userFound);
        emailService.sendResetPasswordToken(resetPasswordToken);
        
        StringBuilder sb = new StringBuilder();
        sb.append("An e-mail has been sent to the addres you have provided.");
        sb.append("Please follow the link in the e-mail to complete you password reset request.");
        
        return ResponseEntity.ok().body(new MessageDTO(sb.toString(),
                                                        HttpStatus.OK.value()
                                                        )
                                        );
    }
    
}

Nesse post vimos como interceptar exeptions sem ter que escrever blocos try catch no resource. Para os fluxos Invalid link e Expired link teríamos que criar duas novas exeptions para tratar esses dois fluxos de exceção assim com qualquer fluxo de exceção personalizado que futuramente precisaremos fazer. 

Para tornar esse trabalho mais simples, criamos um DTO chamado MessageDTO onde passamos a mensagem e o código http de retorno. Isso vai simplificar bastante o tratamento de exceções personalizadas. 

package com.br.davifelipe.springjwt.dto;

import java.io.Serializable;
import java.net.URI;
import java.util.Date;

import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import lombok.Data;

@Data
public class MessageDTO implements Serializable{
    
    private static final long serialVersionUID = 1L;
    
    private Long timestamp;
    private Integer status;
    private String error;
    private String message;
    private String path;
    
    private void setValues(String message,String error,Integer status) {
        URI uri = ServletUriComponentsBuilder
                  .fromCurrentRequest().build().toUri();
        
        this.status = status;
        this.timestamp = new Date().getTime();
        this.message = message;
        this.path = uri.getRawPath();
        this.error = error;
    }
    
    public MessageDTO(String message,String error,Integer status) {
        this.setValues(message, error, status);
    }
    
    public MessageDTO(String message,Integer status) {
        this.setValues(message, null, status);
    }    
}

Nesse outro post mostramos como criar uma regra de validação personalizada. Em ChangePasswordDTO criamos uma regra personalizada para validar se a nova senha e a confirmação da nova senha conferem

package com.br.davifelipe.springjwt.dto;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import com.br.davifelipe.springjwt.services.validation.ChagePasswordSave;
import com.fasterxml.jackson.annotation.JsonProperty;

import lombok.Data;

@Data
@ChagePasswordSave
public class ChangePasswordDTO {
    
    @NotNull
    @Size(min=10)
    private String token;
    
    @NotNull
    @Size(min=2)
    private String password;
    
    @NotNull
    @Size(min=2)
    @JsonProperty("password_confirm")
    private String passwordConfirm;
    
}

Criamos a anotação personalizada ChagePasswordSave

package com.br.davifelipe.springjwt.services.validation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Constraint(validatedBy = ChangePasswordSaveValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface ChagePasswordSave {
    String message() default "Validation error";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

E a regra de validação

package com.br.davifelipe.springjwt.services.validation;

import java.util.ArrayList;
import java.util.List;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import com.br.davifelipe.springjwt.dto.ChangePasswordDTO;
import com.br.davifelipe.springjwt.resources.exceptions.FieldMessage;

public class ChangePasswordSaveValidator implements ConstraintValidator<ChagePasswordSave, ChangePasswordDTO> {
    @Override
    public void initialize(ChagePasswordSave ann) {
    }

    @Override
    public boolean isValid(ChangePasswordDTO objDto, ConstraintValidatorContext context) {
        List<FieldMessage> erros = new ArrayList<>();
        
        //Make custom validations here and add that in que erros list 
        if(!objDto.getPassword().equals(objDto.getPasswordConfirm())) {
            erros.add(new FieldMessage("password","passord and password confirm not match!"));
        }
        
        for (FieldMessage e : erros) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(e.getMessage()).addPropertyNode(e.getFieldName())
                    .addConstraintViolation();
        }
        return erros.isEmpty();
    }
}

Finalmente podemos testar. Nesse caso vou deixar o profile ativo como test para testar sem envio SMTP. Vou pegar o token pelo console.

spring.profiles.active = test

Agora já podemos subir a aplicação, criar um novo usuário

Imagem

Solicitar o link para mudar a senha

Imagem

Pegar o token pelo console e fazer a requisição passando o token, a nova senha e a confirmação da nova senha.

Lembrando que essa requisição será feita por uma página front end.

Imagem 

Repositório do projeto https://github.com/davifelipems/spring-backend-template/tree/forgot_password

Comentários

 

Quem Sou

Graduado em ADS (Análise e desenvolvimento de sistemas).

Não sou "devoto" de nenhuma linguagem de programação. Procuro aproveitar o melhor de cada uma de acordo com a necessidade do projeto. Prezo por uma arquitetura bem feita, código limpo, puro e simples! 

anuncio atendente