Spring boot sing up

Fala pessoal! Nesse post mostramos como configurar o Spring security. Agora precisamos criar a parte de autenticação e autorização. Para isso vamos usar o JWT com Spring security. Mas Antes de implementar JWT vamos criar um endpoint para que novos usuário possam se inscrever.

Mas antes disso, vamos possibilitar que esse endpoint possa ser habilitado e desabilitado apenas alterando um parâmetro no arquivo de configuração. Para isso vamos criar uma parâmetro personalizado que vai indicar se o endpoint está habilitado ou não. Primeiramente vamos adicionar esse dependencia no pom.xml

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

Em seguida criar essa classe no pacote config

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 ConfigProperties {
    
    private boolean publicSingUpUrlEnable;
 
}

Defini que o prefixo da minha propriedade personalizada vai ser aut e o nome da propriedade vai ser publicSingUpUrlEnable

Agora basta adicionar o novo parametro no arquivo application-dev.properties que o STS já identifica no auto complete

Imagem

Dessa forma defini que esse endpoint vai está habilitado no ambiente de desenvolvimento. Isso possibilita habilitar ou desabilitar essa propriedade em outros ambientes. 

Feito isso, vamos implementar a regra que lê esse parâmetro e aplica a regra de acesso de acordo com o que foi definido. Essas regras estão na classe SecurityConfig

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.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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@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
    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("/products/**");
        
        String[] activeProfiles = env.getActiveProfiles();
        
        if (Arrays.asList(activeProfiles).contains("dev")) {
            //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.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
    
    @Bean
    CorsConfigurationSource configurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
    
    @Bean
    public BCryptPasswordEncoder cCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Dividimos os PUBLIC_MATCHERS em 3 listas para controlar o que vai ser público total, somente GET e somente POST. No caso do endpoint sing-up ele será público somente via POST, por isso adicionamos ele somente da lista PUBLIC_MATCHERS_POST e somente se o valor da propriedade publicSingUpUrlEnable estiver true.

Em seguida emplementamos Bean para que o BCryptPasswordEncoder possa ser injetado em outras classes.

Agora precisamos criar o User model, Service e Repository

package com.br.davifelipe.springjwt.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

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

@Entity
@Data
@NoArgsConstructor
@ToString(exclude="id")
@EqualsAndHashCode(exclude={"name","email","password"})
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private String email;
    private String password;
}

Repository

package com.br.davifelipe.springjwt.repositories;

import org.springframework.data.jpa.repository.JpaRepository;

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

public interface UserRepository extends JpaRepository<User, Integer>{

}

Service

package com.br.davifelipe.springjwt.services;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import com.br.davifelipe.springjwt.model.User;
import com.br.davifelipe.springjwt.repositories.UserRepository;

@Service
public class UserService {
    
    @Autowired
    private BCryptPasswordEncoder encoder;
    
    @Autowired
    private UserRepository repo;
    
    /**
     * Find User by id
     * @param User id of the object
     * @return object found or null if the object were not found
     * */
    public User findById(Integer id) {
        Optional<User> obj = this.repo.findById(id);
        return obj.orElse(null);
    }
    
    /**
     * Insert a new user
     * @param User user to be inserted
     * @return User object inserted 
     * */
    public User insert(User obj) {
        obj.setId(null);
        obj.setPassword(encoder.encode(obj.getPassword()));
        return this.repo.save(obj);
    }
}

No service, injetamos o objeto BCryptPasswordEncoder encoder para criptografar a senha na hora de inserir o usuário. 

Antes de criar o Resource, devemos criar o DTO. Normalmente o DTO de usuário não tem o atributo senha. Por que não é interessante trafegar objetos pela rede com a senha do usuário, mesmo criptografada. Mas nesse caso, como vamos cadastrar um novo usuário, precisamos enviar a senha. Então vamos criar um DTO específico para essa situação chamado SingUpDTO que só vai servi para fazer o cadastro do usuário. Caso seja necessário no futuro listar os usuários sem a senha, criaremos outro DTO(sem senha) para isso.

package com.br.davifelipe.springjwt.dto;

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

import lombok.Data;

@Data
public class SingUpDTO {
    
    @NotNull
    @Size(min=2, max=30)
    private String name;
    
    @NotNull
    @Email
    private String email;
    
    @NotNull
    @Size(min=2)
    private String password;
}

Esse DTO não tem id por que ele foi feito exclusivamente para cadastrar um novo usuário e não precisa de id para isso.

Finalmente vamos criar o Resource para receber a requisição, chamar o service e inserir um novo usuário na base de dados

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.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.SingUpDTO;
import com.br.davifelipe.springjwt.model.User;
import com.br.davifelipe.springjwt.services.UserService;

@RestController
@RequestMapping("/auth")
public class AuthResource {
    
    @Autowired
    private UserService service;
    
    @PostMapping("/sing-up")
    public ResponseEntity<Void> singUp(@Valid @RequestBody SingUpDTO dto){
        
        ModelMapper modelMapper = new ModelMapper();
        User user = modelMapper.map(dto,User.class);
        
        user = this.service.insert(user);
        URI uri = ServletUriComponentsBuilder
                  .fromCurrentContextPath().path("/{id}")
                  .buildAndExpand(user.getId())
                  .toUri();
        return ResponseEntity.created(uri).build();
    }
    
}

O resource recebe um objeto SingUpDTO, fazemos o mapeamento do DTO para objeto user e chamamos o service para inserir na base de dados.

Por padrão, o resource deve retornar uma url para consultar o usuário que foi inserido. Mas isso pode ser feito depois. 

Agora vamos testar

Imagem

no console do STS deve aparecer o sql insert 

Imagem

E no h2 o registro de usuário com a senha criptografada

Imagem

Não podemos esquecer de parar a aplicação, alterar a propriedade auth.public-sing-up-url-enable para false e verificar se o endpoint está sendo bloqueado

Imagem

Dessa forma podemos bloquear e liberar acesso ao cadastro de novos usuário apenas alterando o valor do arquivo properties de cada ambiente.

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