Este guia fornecerá toda a teoria e os códigos completos para que os alunos implementem um sistema de segurança robusto do zero.
Objetivo: Proteger nossa API REST, garantindo que apenas usuários autenticados e autorizados possam acessar os recursos. Ao final deste módulo, o aluno terá implementado um fluxo de segurança stateless completo, utilizando o padrão JWT.
Conceito-Chave: Stateful (Baseado em Sessão)
É o modelo tradicional. Quando você faz login em um site, o servidor cria uma “sessão” para você, guarda essa informação em sua memória e te devolve um identificador (um Session ID, geralmente em um cookie). A cada nova requisição, você envia esse ID, e o servidor o usa para “lembrar” quem você é.
Conceito-Chave: Stateless (Baseado em Token) É o modelo moderno, ideal para APIs. Quando você faz login, o servidor não guarda nada. Em vez disso, ele te entrega um “passaporte” criptografado e autossuficiente: o JWT. Este token contém todas as informações necessárias para provar quem você é. A cada nova requisição, você apresenta esse passaporte.
O que é um JWT (JSON Web Token)?
É um padrão (RFC 7519) que define um formato compacto e seguro para transmitir informações entre partes como um objeto JSON. Ele consiste em três partes separadas por pontos (.):
sequenceDiagram
participant Client as 🖥️ Cliente (SPA)
participant Server as ☁️ Servidor (API REST)
Client->>Server: 1. POST /api/auth/login (usuário, senha)
Server->>Server: 2. Valida as credenciais no banco
alt Credenciais Válidas
Server->>Server: 3. Gera um Token JWT
Server-->>Client: 4. Retorna o Token JWT (200 OK)
else Credenciais Inválidas
Server-->>Client: 401 Unauthorized
end
Client->>Client: 5. Armazena o Token (ex: localStorage)
Note over Client, Server: Para requisições futuras...
Client->>Server: 6. GET /api/produtos (Header: "Authorization: Bearer <token>")
Server->>Server: 7. Filtro de Segurança intercepta e valida o Token
alt Token Válido
Server->>Server: 8. Permite acesso ao Controller
Server-->>Client: 9. Retorna os dados (200 OK)
else Token Inválido/Expirado
Server-->>Client: 403 Forbidden
end
O diagrama acima trata “Servidor” como uma caixa-preta. Na prática, o login passa por 3 colaboradores reais dentro do Spring:
sequenceDiagram
participant SPA as 🖥️ auth.js
participant AC as AuthenticationController
participant AM as AuthenticationManager
participant UDS as DatabaseUserDetailsService
participant JTP as JwtTokenProvider
SPA->>AC: POST /api/auth/login {login, password}
AC->>AM: authenticate(UsernamePasswordAuthenticationToken)
AM->>UDS: loadUserByUsername(login)
UDS-->>AM: UserDetails (login, senha hash, papéis)
AM->>AM: compara senha via PasswordEncoder (BCrypt)
alt Senha confere
AM-->>AC: Authentication (autenticado)
AC->>JTP: generateToken(userDetails)
JTP-->>AC: token JWT assinado
AC-->>SPA: 200 OK { token }
else Senha não confere
AM-->>AC: BadCredentialsException
AC-->>SPA: 401 Unauthorized
end
sequenceDiagram
participant SPA as 🖥️ api.js
participant Filter as JwtAuthenticationFilter
participant JTP as JwtTokenProvider
participant UDS as DatabaseUserDetailsService
participant Ctrl as ProdutoController
SPA->>Filter: GET /api/produtos (Header Authorization: Bearer token)
Filter->>JTP: extractUsername(token)
JTP-->>Filter: login
Filter->>UDS: loadUserByUsername(login)
UDS-->>Filter: UserDetails
Filter->>JTP: isTokenValid(token, userDetails)
alt Token válido
JTP-->>Filter: true
Filter->>Filter: SecurityContextHolder.setAuthentication(...)
Filter->>Ctrl: encaminha a requisição (filterChain.doFilter)
Ctrl-->>SPA: 200 OK [produtos]
else Token inválido/expirado
JTP-->>Filter: false
Filter->>Ctrl: encaminha sem autenticação
Ctrl-->>SPA: 403 Forbidden
end
JwtTokenProvider (O Gerador de Tokens)Conceito-Chave: Esta classe será nosso canivete suíço para todas as operações relacionadas ao JWT. Ela será responsável por gerar, validar e extrair informações dos tokens.
Ação: Crie o subpacote config.security e adicione a classe JwtTokenProvider.
Ação 2: Adicione as configurações do JWT ao application.properties.
src/main/resources/application.properties (Adições)# Configurações do JWT
# IMPORTANTE: Em produção, use uma chave MUITO mais complexa e guarde-a de forma segura (ex: variáveis de ambiente).
jwt.secret=7d4a1b0b3e6c9d8a3f4b2e1c0a9f8b7c6d5e4a3b2c1d0f9e8a7b6c5d4e3f2a1b
jwt.expiration-in-ms=3600000 # 1 hora = 3.600.000 ms
config/security/JwtTokenProvider.javapackage br.com.aula.gestaodeestoques.config.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Service
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration-in-ms}")
private long jwtExpiration;
// Extrai o nome de usuário (subject) do token
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// Extrai um "claim" (informação) específico do token
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
// Gera um novo token JWT para o usuário
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
// Gera um token com "claims" extras
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return Jwts
.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
// Valida se o token é válido para o usuário e se não está expirado
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
// Verifica se o token expirou
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
// Extrai a data de expiração do token
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// Decodifica e extrai todas as informações do corpo (payload) do token
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
// Obtém a chave de assinatura a partir da nossa chave secreta
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}
JwtAuthenticationFilter)Conceito-Chave: Este filtro é o nosso “segurança na porta”. Ele intercepta toda requisição que chega à API, verifica se ela tem um passaporte (JWT) válido e, em caso afirmativo, libera o acesso.
Ação: Crie a classe JwtAuthenticationFilter no pacote config.security.
config/security/JwtAuthenticationFilter.javapackage br.com.aula.gestaodeestoques.config.security;
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.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String username;
// 1. Verifica se o header de autorização existe e se começa com "Bearer "
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response); // Se não, passa para o próximo filtro
return;
}
// 2. Extrai o token do header
jwt = authHeader.substring(7);
username = jwtTokenProvider.extractUsername(jwt);
// 3. Se o token tem um usuário e o usuário ainda não está autenticado no contexto de segurança atual
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 4. Valida o token
if (jwtTokenProvider.isTokenValid(jwt, userDetails)) {
// Se o token for válido, cria um objeto de autenticação...
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// ...e o define no contexto de segurança. O usuário agora está autenticado para esta requisição.
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
// 5. Passa a requisição para o próximo filtro na cadeia
filterChain.doFilter(request, response);
}
}
SecurityConfig para uma API StatelessAção: Agora vamos reconfigurar o coração do Spring Security. Vamos remover a configuração de formLogin, desabilitar sessões e “encaixar” nosso JwtAuthenticationFilter no lugar certo.
config/SecurityConfig.java (Arquivo Reescrito)package br.com.aula.gestaodeestoques.config;
import br.com.aula.gestaodeestoques.config.security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
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
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthFilter;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable) // Desabilitar CSRF para APIs stateless
.authorizeHttpRequests(req -> req
// Endpoints públicos não precisam de autenticação
.requestMatchers("/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
// Todas as outras requisições precisam de autenticação
.anyRequest().authenticated()
)
// Configura a gestão de sessão para ser STATELESS
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// Define o provedor de autenticação que usaremos
.authenticationProvider(authenticationProvider())
// Adiciona nosso filtro JWT para ser executado ANTES do filtro padrão de usuário/senha
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService); // Nosso serviço que busca usuário no banco
authProvider.setPasswordEncoder(passwordEncoder()); // Nosso codificador de senhas
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
AuthenticationController)Ação: Finalmente, criamos o endpoint /api/auth/login que o cliente usará para trocar suas credenciais por um token JWT.
dto/auth/LoginRequest.javapackage br.com.aula.gestaodeestoques.dto.auth;
public record LoginRequest(String login, String password) {}
dto/auth/JwtAuthenticationResponse.javapackage br.com.aula.gestaodeestoques.dto.auth;
public record JwtAuthenticationResponse(String token) {}
controller/AuthenticationController.javapackage br.com.aula.gestaodeestoques.controller;
import br.com.aula.gestaodeestoques.config.security.JwtTokenProvider;
import br.com.aula.gestaodeestoques.dto.auth.JwtAuthenticationResponse;
import br.com.aula.gestaodeestoques.dto.auth.LoginRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
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;
@RestController
@RequestMapping("/api/auth")
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@PostMapping("/login")
public ResponseEntity<JwtAuthenticationResponse> authenticate(@RequestBody LoginRequest request) {
// 1. Tenta autenticar o usuário usando o mecanismo do Spring Security
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.login(), request.password())
);
// 2. Se a autenticação for bem-sucedida, o objeto Authentication conterá os detalhes do usuário
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// 3. Gera o token JWT
String token = jwtTokenProvider.generateToken(userDetails);
// 4. Retorna o token na resposta
return ResponseEntity.ok(new JwtAuthenticationResponse(token));
}
}
Parabéns! Você acaba de implementar um sistema de segurança de nível profissional. Neste ponto, nossa API está:
Agora, você pode usar uma ferramenta como o Postman para:
POST em http://localhost:8080/api/auth/login com o corpo {"login": "admin", "password": "admin123"}.GET em http://localhost:8080/api/produtos, adicionando um Header Authorization com o valor Bearer <seu_token_copiado>.No próximo módulo, vamos tornar nossa API fácil de ser explorada e consumida, implementando a documentação interativa com OpenAPI/Swagger e aprimorando nosso tratamento de erros.