Autenticação com Spring Security e JWT: O que mudou
- 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)

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