Como Aplicações Web se Tornam Seguras: JWT na Prática

Imagem de capa: 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.JwtBearer

Geraçã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 isso

Como 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 assinatura

Em 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@nLwY4vBtJhE6cFdAuNs

Regras 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, .env fora 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

  • Estado no servidor: Nenhum (stateless)
  • Escalabilidade: Alta — sem consulta ao DB
  • Revogação imediata: Difícil — depende da expiração
  • Complexidade: Média
  • Melhor para: APIs REST, microsserviços, mobile
  • Risco principal: Token roubado = acesso até expirar

Sessions (server-side)

  • Estado no servidor: Sim (session store)
  • Escalabilidade: Requer sessão compartilhada
  • Revogação imediata: Fácil — apaga a sessão
  • Complexidade: Baixa
  • Melhor para: Aplicações web monolíticas
  • Risco principal: Session hijacking

OAuth 2.0

  • Estado no servidor: Depende do flow
  • Escalabilidade: Alta
  • Revogação imediata: Fácil (tokens opacos)
  • Complexidade: Alta
  • Melhor para: Login social, APIs de terceiros
  • Risco principal: Configuração incorreta
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

Gostou deste artigo? Compartilhe!