top of page

Autenticação com Spring Security e JWT: O que mudou

  • Foto do escritor: Eduardo Nepomuceno da Rocha
    Eduardo Nepomuceno da Rocha
  • 19 de nov. de 2023
  • 8 min de leitura

Visto anos atrás como uma linguagem com dias contados no mercado, o Java segue utilizado em grandes empresas que utilizam soluções de software para seu cotidiano, e ainda está entre as linguagens mais citadas e vistas em códigos nos relatórios do Github (segundo o último trimestral de 2022, era a segunda mais usada, estando atrás somente do Python). O Spring Boot é o fator que pesa para que Java siga utilizado em massa, devido à necessidade de implementações WEB, já que ele é um grande facilitador para criações de APIs, arquiteturas de microsserviços, e outras abordagens.



JSON WEB TOKEN(JWT)



ree

A ideia da tecnologia JWT é muito simples. Após o usuário ser criado, ele deve se autentificar para fazer requisições para as quais o API exigirá um token. Ao enviar um Post para um endpoint, ele receberá esse token. Após esse momento, ele deve ser sempre utilizado no Header das requisições sob o parâmetro de Authorization. A API verifica se o token é válido e retorna os dados da requisição.


O Spring Security vem com um conjunto de ferramentas, funções e configurações que possibilitam utilizar o JWT. No entanto, o security passa por utillizações constantes e na internet há diversos tutoriais, que, ao longo do tempo, acabam por não ser úteis a um desenvolvedor que esteja procurando como implementar na sua aplicação. Além disso, o groupId io.jsonwebtoken perdeu espaço para outros groupid, como o auth8. Entretanto, muitos dos tutoriais na internet ainda utilizam a primeira tecnologia citada. Nesse texto mostrarei como utilizei recentemente o JWT para autenticar uma API de um projeto no qual atuo. O link da aplicação no github ficará disponibilizado ao fim do texto. Nessa aplicação, a autenticação será um endpoint para login de usuário. Boas práticas como DTO não foram praticadas como um todo devido ao tempo curto.



1 - Criação da entidade usuário e repositório


package com.mindsim.petroapi.entities;
import com.mindsim.petroapi.entities.enums.UserRole;
import jakarta.persistence.*;import lombok.AllArgsConstructor;
import lombok.Data;import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;

@Entity@Table(name = "usuarios")
@AllArgsConstructor
@NoArgsConstructor
@Data
@SequenceGenerator(name = "generator_usuario", sequenceName = "sequence_usuario", initialValue = 1, allocationSize =1)

public class Usuario implements UserDetails {    
@Id    
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "generator_usuario")    
private Integer id;    
@Column(nullable = false, unique = true)    
private String email;    
@Column(nullable = false)    
private String senha;    
@Enumerated(EnumType.STRING)    
@Column(name = "perfil", nullable = false)    
private UserRole role;    

@Override    
public Collection<? extends GrantedAuthority> getAuthorities() {        
if(this.role==UserRole.ADMIN){            
return List.of(new SimpleGrantedAuthority("ROLE_ADMIN"), new SimpleGrantedAuthority("ROLE_USER"));        
}else {
            return List.of(new SimpleGrantedAuthority("ROLE_USER"));        
      }    
}    

@Override    
public String getPassword() { 
       return senha;    
      }    

@Override   
public String getUsername() {
        return email;    
      }    

@Override    
public boolean isAccountNonExpired() {
        return true;    
      }    

@Override    
public boolean isAccountNonLocked() {       
        return true;    
      }    

@Override    
public boolean isCredentialsNonExpired() { 
       return true;    
      }    

@Override    
public boolean isEnabled() {        
return true;    
      }

}

Essa é a entidade que será armazenada no banco de dados. Ela deve implementar a interface UserDetails. É essencial que os métodos booleans sejam colocados como true caso não utilize uma função específica, e o getPassword e Username também devem ser reescritos. Caso não seja feito, o momento de autorização da API falhará. A enumeração utilizada é mostrada a seguir:


public enum UserRole {    
ADMIN("admin"),    
USER("user");    

private String role;    
UserRole(String role){        
this.role=role;    
}    
public String getRole(){        
return role;    
}
}

O repositório, como qualquer aplicação de repositório do Java, é uma interface implementando o JPARepository. Importante lembrar as bibliotecas que devem ser aplicadas para que tenha UserDetails e outras funcionalidades do spring security. No pom do meu projeto, o leitor encontrará:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
   <version>3.1.1</version>
</dependency>
<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.1</version>
</dependency>
public interface UsuarioRepository extends JpaRepository<Usuario, Integer> {
    Optional<Usuario> findByEmail(String email);
}

A organização de pastas dentro do projeto é melhor de ser vista no Github, mas também é uma questão de escopo e preferência do desenvolvedor.



2 - Componente para gerenciamento de tokens


Nessa etapa algumas mudanças já são necessárias em relação ao padrão anterior do security. O método signWith passou a apresentar uma exceção de ClassNotFoundException. Resolvi na empresa acrescentando um trecho no pom, com mais uma configuração no properties.


<dependency>
   <groupId>javax.xml.bind</groupId>
   <artifactId>jaxb-api</artifactId>
   <version>2.3.1</version>
</dependency>
spring.jaxb.enabled=true

O médodo generateToken recebe uma autenticação e extrai dela o usuário, construindo o token com uma expiração de 2h, parâmetro calculado em ms. É utilizado um algoritmo de criptografia, o mais comum o HS512 e a secret é a chave que gera esse token aleatório. Repare que o setSubject é feito em cima do id, então o método seguinte vai obter o id do usuário do token, extraindo pelo método privado da classe.

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Optional;

@Component
public class JWTTokenService {
    
    private static final String SECRET = "secret";
    private final int EXPIRATION = 7200000;//2 horas
    
   public String generateToken(Authentication authentication){
        Usuario usuario = (Usuario) authentication.getPrincipal();
        return Jwts.builder()
                    .setSubject(usuario.getId().toString())
                    .setIssuedAt(new Date())
                    .setExpiration(new Date(new Date().getTime() + EXPIRATION))
                    .signWith(SignatureAlgorithm.HS512, SECRET)
                    .compact();
    }
    public Optional<Integer> obtainIdFromUsuario(String token){
        try{
            Claims claims = extractClaimsFromToken(token).getBody();
            return Optional.ofNullable(Integer.parseInt(claims.getSubject()));
        }catch (Exception e){
            return Optional.empty();
        }
    }    //metodo que extrai do token as permissoes do usuario
    private Jws<Claims> extractClaimsFromToken (String token){
        return Jwts
                .parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token);
    }
}

3 - Serviço do Usuário


Essa etapa alguns desenvolvedores preferem não implementar e partir para a implementação da autenticação sem o service, utilizando o repository diretamente lá. Eu particularmente prefiro a estrutura com repository, service e controller, pois acho que separa melhor as camadas do projeto. Como a API já havia sido iniciada nessa estrutura, optei por fazer o mesmo com usuários. De diferente de um CRUD padrão, temos a injeção do passwordEncoder, que será apresentado na configuração do Spring Security. Ele é um Bean que permite criptografar a senha que será enviada ao banco. Antes de salvar a senha, ela deve passar por esse bean, para que não seja visível na tabela do banco. O authentication Manager também é um bean instanciado na configuração. A autenticação é passada para o contexto da aplicação e um token é gerado para retorno. A partir desse token gerado, ele será utilizado para demais requisições que exijam autenticação. O token é do tipo "Bearer " + "string", mas há outros tipos de acordo com a criptografia usada.

import com.mindsim.petroapi.entities.Usuario;
import com.mindsim.petroapi.repositories.UsuarioRepository;
import com.mindsim.petroapi.security.services.JWTTokenService;
import com.mindsim.petroapi.shared.dto.LoginDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.InputMismatchException;
import java.util.List;
import java.util.Optional;

@Service
public class UsuarioService {
    private static final String HEADER_PREFIX = "Bearer ";
    @Autowired
    private UsuarioRepository usuarioRepository;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private JWTTokenService jwtTokenService;
    @Autowired
    private AuthenticationManager authenticationManager;
    
    public List<Usuario> findAll(){
        return usuarioRepository.findAll();
    }
    public Optional<Usuario> findById(Integer id){
        return usuarioRepository.findById(id);
    }
    public Optional<Usuario> findByEmail(String email){
        return usuarioRepository.findByEmail(email);
    }
    public Usuario adicionar(Usuario usuario){
        usuario.setId(null);
        if(findByEmail(usuario.getEmail()).isPresent()){
            throw new InputMismatchException("Ja existe usuario cadastro com esse email");
        }
        usuario.setSenha(passwordEncoder.encode(usuario.getSenha()));         return usuarioRepository.save(usuario);
    }
    public LoginDTO doLogin(String email, String senha){
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(email,senha);
        try{
            Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);            SecurityContextHolder.getContext().setAuthentication(authentication);
            String token = HEADER_PREFIX + jwtTokenService.generateToken(authentication);
            Usuario usuario = usuarioRepository.findByEmail(email).get();
            return new LoginDTO(token, usuario);
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
        return null;
    }
}

4 - Classe auxiliar do serviço de usuário


Essa classe costuma, por padrão, ser denominada algo como "CustomUserDetailsService" ou "CustomUserService", uma nomenclatura específica próxima a um nome que deixe claro que essa classe interage com a de serviço do usuário. No entanto, implementei com nome de autenticação no momento do projeto. Vale lembrar que, a depender da arquitetura, não haverá um service específico da entidade usuário. É uma classe utilizada para implementar a interface UserDetails. Implementei o método loadUserByUsername associando o username ao email e utilizando um supplier, que utiliza uma função lambda que retorna um optional, e é uma prática bastante comum no mercado

import com.mindsim.petroapi.entities.Usuario;
import com.mindsim.petroapi.repositories.UsuarioRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.function.Supplier;

@Service
public class AuthenticationService implements UserDetailsService {
    @Autowired
    private UsuarioRepository usuarioRepository;
    @Override    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return UsuarioByFunction(()->usuarioRepository.findByEmail(email));
    }
    private UserDetails UsuarioByFunction(Supplier<Optional<Usuario>> supplier) {
        return supplier.get().orElseThrow(() -> new UsernameNotFoundException("Usuario nao encontrado"));
    }
}

5 - Implementando o filtro de autenticação


O filtro de autenticação herda a classe OncePerRequestFilter, que é uma classe pela qual passará toda requisição da aplicação. O método doFilterInternal é por onde passará cada requisição. No método implementado tenta-se obter um token. Se existir token, um usuário autenticado está acessando nossa aplicação. Caso seja null, avança para a próxima requisição, pois será uma requisição que não exige autenticação, ou simplesmente uma requisição inválida/não autorizada. Em caso de haver autenticação, a aplicação encontra o usuário e passa sua autenticação para o contexto da aplicação.

import com.mindsim.petroapi.entities.Usuario;
import com.mindsim.petroapi.repositories.UsuarioRepository;
import com.mindsim.petroapi.security.services.AuthenticationService;
import com.mindsim.petroapi.security.services.JWTTokenService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Optional;

@Component
public class SecurityFilter extends OncePerRequestFilter {
    @Autowired
    private AuthenticationService authenticationService;
    @Autowired
    private JWTTokenService jwtTokenService;
    @Autowired
    private UsuarioRepository usuarioRepository;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        var token = getToken(request);
        if(token!=null){
            Optional<Integer> id = jwtTokenService.obtainIdFromUsuario(token);
            if(id.isPresent()) {
                Usuario usuario = usuarioRepository.findById(id.get()).get();
                UserDetails userDetails = authenticationService.loadUserByUsername(usuario.getEmail());
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        filterChain.doFilter(request, response);
    }
    private String getToken(HttpServletRequest request){
        String token = request.getHeader("Authorization");
        if(!StringUtils.hasText(token)){
            return null;
        }
        return token.substring(7);
    }
}

6 - Configuração do Security


Essa etapa com certeza foi a que mais mudou ao longo de atualizações do Spring. Anteriormente herdando de uma classe de Web Security, depois migrando para programação funcional em série. Atualmente a configuração das requisições http é feita em cima de requestMatchers dentro de uma função lambda, assim como outras configurações. Por exemplo: não existe mais csrf().disable, mas o csrf(c->c.disable()), ou seja, chamadas em série agora dão lugar a funções lambda. Na configuração dessa API, permitimos todas as requisições para a documentação do Swagger, todos os posts para usuário e login(criar um usuário e se autenticar), e nos dados influx, apenas papel de ADMIN. Todas as outras requisições devem estar ao menos autenticadas. Temos duas funcionalidades Bean, que são o codificador para senha e o objeto de autenticação do Spring.

import com.mindsim.petroapi.security.filters.SecurityFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration@EnableWebSecurity
public class SecurityConfiguration {
    @Autowired
    private SecurityFilter securityFilter;
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Bean
    public SecurityFilterChain securityFilterChain (HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .csrf(csrf -> csrf.disable())
                .sessionManagement(s-> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth->
                        auth
                                .requestMatchers("/v3/api-docs/**").permitAll()
                                .requestMatchers("/swagger-ui/**").permitAll()
                                .requestMatchers("/swagger-ui.html").permitAll()
                                .requestMatchers(HttpMethod.POST, "/v1/petroapi/usuarios").permitAll()
                                .requestMatchers(HttpMethod.POST, "/v1/petroapi/usuarios/login").permitAll()
                                .requestMatchers(HttpMethod.POST, "/v1/petroapi/influx").hasRole("ADMIN")
                                .anyRequest().authenticated()                )
                .addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class)
                .build();
    }
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

7 - Controlador do usuário


É basicamente fazer o padrão de aplicar o service dentro do controller. Essa versão simples do projeto ainda está sem exceções, que no geral acabam seguindo um padrão um pouco pessoal do desenvolvedor. As classes DTO, Response e Request são mostradas posteriormente. Tenho por hábito trabalhar com DTO na camada de service, enquanto utilizo uma pasta para tráfego no controller com sufixos response e request.

import com.mindsim.petroapi.entities.Usuario;
import com.mindsim.petroapi.services.UsuarioService;
import com.mindsim.petroapi.shared.LoginRequest;
import com.mindsim.petroapi.shared.LoginResponse;
import com.mindsim.petroapi.shared.dto.LoginDTO;
import jakarta.validation.Valid;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;

@CrossOrigin("*")
@RestController
@RequestMapping("v1/petroapi/usuarios")
public class UsuarioController {
    @Autowired
    private UsuarioService usuarioService;
    @GetMapping
    public ResponseEntity<List<Usuario>> findAll(){
        List<Usuario> usuarios = usuarioService.findAll();
        return new ResponseEntity<>(usuarios, HttpStatus.OK);
    }
    @GetMapping("/{id}")
    public ResponseEntity<Optional<Usuario>> findById(@PathVariable("id") Integer id){
        return new ResponseEntity<>(usuarioService.findById(id), HttpStatus.OK);
    }
    @PostMapping
    public ResponseEntity<Usuario> create(@Valid @RequestBody Usuario usuario){
        Usuario user = usuarioService.adicionar(usuario);
        return new ResponseEntity<>(user, HttpStatus.CREATED);
    }
    @PostMapping("/login")
    public ResponseEntity<LoginResponse> login (@RequestBody LoginRequest request){
        LoginDTO loginDTO = usuarioService.doLogin(request.getEmail(), request.getSenha());
        LoginResponse loginResponse = new ModelMapper().map(loginDTO, LoginResponse.class);
        return new ResponseEntity<>(loginResponse, HttpStatus.OK);
    }
}

@AllArgsConstructor
@NoArgsConstructor
@Data
public class LoginDTO {
    private String token;
    private Usuario usuario;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginRequest {
    private String senha;
    private String email;
}
@Data@
AllArgsConstructor
@NoArgsConstructor
public class LoginResponse {
    private String token;
    private Usuario usuario;
}

Link do projeto no Github: Clique aqui

Comentários


bottom of page