Como Aplicações Web se Tornam Seguras: JWT na Prática
Quando comecei a construir a API de autenticação do meu projeto de gestão de tarefas em Spring Boot, minha primeira pergunta foi: como garantir que cada requisição venha realmente do usuário que fez login? Sessões simples funcionavam localmente, mas não escalavam bem com múltiplas instâncias. Foi aí que me aprofundei em JWT — e aprendi tanto os benefícios quanto as armadilhas da abordagem. Este post reúne o que eu gostaria de ter encontrado naquela época.
O problema: autenticação em múltiplas requisições
Muitas aplicações Web são totalmente funcionais sem login — a pesquisa do Google, a Wikipedia e o Stack Overflow não exigem contas. Por outro lado, Amazon, Facebook e a maioria dos sistemas corporativos exigem autenticação em praticamente toda interação.
O desafio é que HTTP é um protocolo sem estado: cada requisição chega ao servidor sem memória do que aconteceu antes. Pedir a senha a cada clique seria insuportável. A solução é emitir um token após o login — uma credencial temporária que o cliente apresenta automaticamente nas requisições seguintes.
Isso levanta algumas perguntas:
- Onde armazenar o token no cliente?
- Como o servidor verifica se ele é legítimo?
- O que impede alguém de forjar ou modificar o token?
- Como revogar acesso antes do token expirar?
O JSON Web Token (JWT) é uma das respostas mais adotadas para essas perguntas.
O que é JWT?
Um JWT é um objeto JSON assinado criptograficamente, gerado no login e usado nas requisições subsequentes até expirar.
A palavra-chave é assinado — não criptografado. Isso é importante e exploraremos as implicações logo adiante.
Assinatura vs. criptografia
Criptografia
- O que faz: Esconde o conteúdo
- Proteção oferecida: Confidencialidade
- No JWT: Não usada
Assinatura
- O que faz: Garante que o conteúdo não foi alterado
- Proteção oferecida: Integridade
- No JWT: Usada
A confidencialidade no transporte é responsabilidade do TLS (o "S" do HTTPS), que criptografa toda a comunicação entre cliente e servidor. O JWT cuida apenas de garantir que ninguém modifique o token sem que o servidor perceba.
Estrutura do JWT
Um JWT tem três partes separadas por ponto:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← Header (algoritmo)
.eyJ1c2VySWQiOiIxMjMiLCJyb2xlIjoiVVNFUiIsImV4cCI6MTcxMjAwMDAwMH0 ← Payload (dados)
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature (assinatura)
O payload decodificado (Base64) seria algo como:
{
"userId": "123",
"username": "joao.silva",
"role": "USER",
"exp": 1712000000
}⚠️ Atenção: o payload é apenas codificado em Base64, não criptografado. Qualquer pessoa com o token consegue ler seu conteúdo. Nunca armazene senhas, dados sensíveis ou informações de cartão em um JWT.
Fluxo completo de autenticação
1. Cliente envia nome de usuário + senha → POST /auth/login
2. Servidor valida as credenciais no banco (hash da senha)
3. Servidor gera o JWT com dados da sessão + assina com o segredo
4. JWT é retornado ao cliente (cookie HttpOnly ou header Authorization)
5. Cliente inclui o JWT em todas as requisições seguintes
6. Servidor verifica a assinatura + expiração antes de processar
Implementação em Spring Boot (Java)
Dependência no pom.xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>Gerando o token após o login
@Service
public class JwtService {
// Nunca hardcode o segredo no código! Leia de variável de ambiente.
@Value("${jwt.secret}")
private String secret;
private static final long EXPIRATION_MS = 1000 * 60 * 60; // 1 hora
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.claim("role", userDetails.getAuthorities())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractClaim(token, Claims::getExpiration).before(new Date());
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return claimsResolver.apply(claims);
}
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
}Filtro de autenticação (middleware)
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired private JwtService jwtService;
@Autowired private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String jwt = authHeader.substring(7);
String username = jwtService.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}Implementação em ASP.NET Core (C#)
Pacote NuGet
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearerGeração do token
public class JwtService
{
private readonly IConfiguration _config;
public JwtService(IConfiguration config) => _config = config;
public string GenerateToken(ApplicationUser user)
{
// Nunca hardcode o segredo! Use User Secrets ou variáveis de ambiente.
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config["Jwt:Secret"]!)
);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Name, user.UserName!),
new Claim(ClaimTypes.Role, user.Role)
};
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}Configuração do middleware em Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"]!)
)
};
});
// Em seguida, no pipeline:
app.UseAuthentication();
app.UseAuthorization();Armadilhas de segurança reais
1. A vulnerabilidade alg: none
Versões antigas de bibliotecas JWT aceitavam um token com o algoritmo declarado como "none" — o que significa que qualquer payload sem assinatura seria aceito como válido.
Token malicioso:
// Header decodificado
{ "alg": "none", "typ": "JWT" }
// Payload decodificado (com role adulterado)
{ "userId": "123", "role": "ADMIN", "exp": 9999999999 }
// Sem assinatura — e bibliotecas vulneráveis aceitavam issoComo se proteger:
Em Spring Boot (JJWT 0.11+), especifique explicitamente os algoritmos aceitos:
Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build() // JJWT moderno rejeita "none" por padrão
.parseClaimsJws(token); // "Jws" (com s) — exige assinatura válida
// NÃO use parseClaimsJwt() — aceita tokens sem assinaturaEm ASP.NET Core, configure explicitamente:
// Vulnerável — aceita qualquer algoritmo que o token declarar
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey
// Sem restrição de algoritmo
};
// Seguro — valida contra lista explícita
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
ValidAlgorithms = new[] { SecurityAlgorithms.HmacSha256 } // força o algoritmo
};2. Segredo fraco ou hardcoded
Um segredo curto pode ser quebrado por força bruta, como demonstrado pelo cracker de JWT publicado por Luciano Mammino.
# Segredo fraco — quebrável em minutos
JWT_SECRET=secret123
# Segredo seguro — 256 bits de entropia
JWT_SECRET=k9#mP2$xQzR7@nLwY4vBtJhE6cFdAuNsRegras práticas:
- Mínimo de 32 caracteres, idealmente 64+
- Gerado aleatoriamente (não uma frase)
- Nunca no código-fonte — use variáveis de ambiente ou cofres de segredo (AWS Secrets Manager, Azure Key Vault,
.envfora do repositório) - Rotacione periodicamente
3. Tokens sem expiração
// Token eterno — se vazado, concede acesso para sempre
Jwts.builder()
.setSubject(username)
.signWith(key)
.compact(); // sem .setExpiration()
// Expira em 1 hora
Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + 3_600_000))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
JWT vs. Sessions vs. OAuth: qual usar?
JWT
Sessions (server-side)
OAuth 2.0
Estado no servidor
Nenhum (stateless)
Sim (session store)
Depende do flow
Escalabilidade
Alta — sem consulta ao DB por requisição
Requer sessão compartilhada entre instâncias
Alta
Revogação imediata
Difícil — depende da expiração
Fácil — apaga a sessão
Fácil (tokens opacos)
Complexidade
Média
Baixa
Alta
Melhor para
APIs REST, microsserviços, mobile
Aplicações web monolíticas tradicionais
Login social, acesso a APIs de terceiros
Risco principal
Token roubado = acesso até expirar
Session hijacking
Configuração incorreta do OAuth flow
Para a maioria das APIs REST com múltiplos serviços ou instâncias, o JWT é uma escolha sólida. Se você tem uma aplicação monolítica simples e precisa revogar acesso imediatamente, sessions server-side podem ser mais simples e suficientes. Para autenticação via Google, GitHub ou outros provedores externos, use OAuth 2.0 — veja como o OAuth funciona para uma explicação detalhada.
O servidor é seguro com JWT?
Sim, com ressalvas. O servidor garante que apenas tokens válidos acessem a API. Mas dois riscos merecem atenção:
Token roubado: qualquer pessoa com uma cópia válida do token consegue se passar pelo usuário legítimo. A mitigação é garantir que o token trafegue apenas por HTTPS, armazená-lo em cookies HttpOnly (inacessíveis a JavaScript) e usar tempos de expiração curtos.
Segredo comprometido: se o segredo usado para assinar os tokens vazar, um atacante pode forjar tokens para qualquer usuário. O segredo nunca deve estar no código-fonte — leia sobre boas práticas de cookies HTTP e segurança para entender onde e como armazenar segredos com segurança.
Vantagens e desvantagens
Vantagens
- Stateless: o servidor não precisa consultar o banco para verificar o token, apenas recomputar a assinatura. Ideal para microsserviços e APIs com múltiplas instâncias.
- Portável: funciona em mobile, SPA, CLIs e qualquer cliente que consiga enviar um header
Authorization. - Autodescritivo: o payload carrega as informações necessárias (userId, role, expiração) sem roundtrip ao banco.
Desvantagens
- Revogação difícil: se um token válido for comprometido, não há como invalidá-lo antes de expirar sem manter uma blacklist no servidor — o que elimina parte do benefício stateless.
- Payload visível: os dados no token são apenas codificados, não cifrados. Informações sensíveis não devem estar no payload.
- Tamanho: tokens JWT são maiores que session IDs simples, o que impacta marginalmente cada requisição.
JWTs são seguros?
A resposta honesta é: depende de como você os usa.
O Google usa JWT para autenticação em suas APIs. O truque está em: usar segredos longos e aleatórios ou algoritmos de assinatura assimétricos (RS256, ES256), manter bibliotecas atualizadas (vulnerabilidades como alg: none já foram corrigidas nas versões modernas), e entender o que o JWT protege e o que não protege.
JWT não faz nada para criptografar dados — apenas garante integridade. Combine JWT com HTTPS, cookies HttpOnly, expiração curta e segredos bem guardados para ter uma implementação realmente segura.
Próximos passos
- Como o OAuth funciona — quando JWT não é suficiente e você precisa de autenticação delegada
- Cookies HTTP e segurança — onde armazenar o token no cliente e as implicações de cada abordagem
- Autenticação de dois fatores (2FA) — camada adicional que protege mesmo quando a senha é comprometida
- Referências técnicas:
Gostou deste artigo? Compartilhe!