JWT: usar corretamente e erros de segurança comuns
1. Visão geral
JWT (JSON Web Token) é usado em toda parte para autenticação de API, mas muitos sistemas implementam de forma incorreta causando vulnerabilidades graves. Este artigo resume como criar e verificar JWT corretamente em Spring Boot, juntamente com erros de segurança clássicos (confusão de alg, secret fraco, não verificar expiration…) e como evitá-los. Após ler, você saberá como configurar JWT com segurança e revisar código JWT de colegas com mais confiança.
2. Estrutura e mecanismo de funcionamento do JWT
JWT possui 3 partes: header.payload.signature, codificadas em base64url, separadas por pontos.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGljZSJ9.4Z... (signature)
- Header: contém
alg(algoritmo de assinatura) etyp. - Payload: contém claims como
sub,exp,iat,roles. - Signature: assinatura para verificar que o token não foi alterado.
Atenção: JWT não encripta o payload, apenas codifica. Qualquer um pode ler o conteúdo se tiver o token. Não coloque senha, PII sensível nos claims.
3. Adicionando dependências
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
A biblioteca jjwt (io.jsonwebtoken) é a mais popular para Java, com API clara e já bloqueia por padrão alguns erros comuns.
4. Criar e verificar token corretamente
4.1. Gerar uma secret key forte
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
public class JwtKeyFactory {
public static SecretKey generateKey() {
return Keys.secretKeyFor(io.jsonwebtoken.SignatureAlgorithm.HS256);
}
}
Keys.secretKeyFor gera uma chave aleatória com comprimento suficiente para HS256 (≥ 256 bits). Em produção, armazene essa chave em um gerenciador de secrets (Vault, AWS Secrets Manager), não no código.
4.2. Criar token com TokenService
import io.jsonwebtoken.Jwts;
import javax.crypto.SecretKey;
import java.time.Instant;
import java.util.Date;
public class TokenService {
private final SecretKey key;
public TokenService(SecretKey key) {
this.key = key;
}
public String generateToken(String username) {
Instant now = Instant.now();
return Jwts.builder()
.subject(username)
.issuedAt(Date.from(now))
.expiration(Date.from(now.plusSeconds(900))) // 15 minutos
.signWith(key)
.compact();
}
}
expirationdefinido para 15 minutos — access token deve ter vida curta.signWith(key)escolhe automaticamente o alg adequado ao tipo de chave, evitando que o cliente interfira noalg.
4.3. Verificar token
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import javax.crypto.SecretKey;
public class TokenVerifier {
private final SecretKey key;
public TokenVerifier(SecretKey key) {
this.key = key;
}
public Claims verify(String token) {
try {
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
} catch (JwtException e) {
throw new InvalidTokenException("Token inválido ou expirado", e);
}
}
}
parseSignedClaims verifica automaticamente a signature e exp. Se a signature estiver errada ou o token expirar, ele lança JwtException — você a captura e converte em uma exceção do seu domínio.
5. Erro de segurança #1: alg=none ou alg confusion
Um atacante altera o header para {"alg":"none"} e envia um token sem signature. Se o backend não forçar um algoritmo, pode aceitar um token falso.
// ERRADO: não especificar qual algoritmo é permitido
Jwts.parser().build().parseSignedClaims(token); // bibliotecas antigas fáceis de enganar
Como evitar — sempre faça whitelist do algoritmo:
import io.jsonwebtoken.Jwts;
Jwts.parser()
.verifyWith(key) // aceita apenas chave conhecida
.build()
.parseSignedClaims(token); // jjwt 0.12+ bloqueia automaticamente alg=none
Atenção: Com outras bibliotecas JWT (jose4j, nimbus-jose-jwt), configure explicitamente o whitelist de algoritmos (por exemplo, apenas
HS256). Nunca confie no campoalgno token enviado pelo cliente.
Uma variante mais perigosa: alg confusion entre RS256 e HS256. Se o servidor verificar com a chave pública RSA mas o código usar como secret para HMAC quando alg=HS256, um atacante pode assinar o token com a chave pública (que é pública). Solução: fixe o algoritmo conforme o caso de uso, não deixe o código ler alg do token e escolher como verificar.
6. Erro de segurança #2: secret fraco ou secret exposto
// ERRADO: secret curto, fácil de fazer brute-force
String secret = "mysecret123";
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());
HS256 requer chave ≥ 256 bits (32 bytes). Secret curto pode sofrer brute-force offline com ferramentas como hashcat em poucas horas.
// CORRETO: secret com comprimento adequado, aleatório
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
Além disso:
- Não faça commit do secret no Git. Use variáveis de ambiente ou gerenciador de secrets.
- Alterne o secret periodicamente, especialmente após funcionários com acesso deixarem a empresa.
# application.properties - lê da variável de ambiente, não hardcode
jwt.secret=${JWT_SECRET}
7. Erro de segurança #3: não verificar expiration ou token com vida muito longa
// ERRADO: access token vive 30 dias
.expiration(Date.from(now.plus(30, ChronoUnit.DAYS)))
Access token com vida muito longa significa que se for vazado, o atacante tem acesso prolongado que você não consegue revogar (JWT é stateless, não tem mecanismo de logout padrão).
Padrão correto: access token curto (5–15 minutos) + refresh token mais longo, armazenado no servidor (ou rotacionado) para permitir revogação.
public class TokenPair {
private final String accessToken; // vive 15 minutos
private final String refreshToken; // vive 7 dias, armazenado em BD para revogação
// construtor, getters
}
public class RefreshTokenRepository {
public boolean isRevoked(String refreshTokenId) {
// consulta BD/Redis, se o token foi revogado (logout) retorna true
return false;
}
}
Quando um refresh token é revogado, o access token antigo continua vivendo até expirar — por isso o access token deve ser curto.
8. Erro de segurança #4: incluir dados sensíveis no payload
// ERRADO: payload é público, quem tem o token consegue ler
.claim("password", user.getPassword())
.claim("creditCard", user.getCardNumber())
O payload apenas codifica em base64, não encripta. Decodificar em jwt.io faz você ver tudo. Coloque apenas claims necessários para autorização: sub, roles, exp, scope.
// CORRETO
.subject(user.getUsername())
.claim("roles", user.getRoles())
9. Erro de segurança #5: não validar issuer/audience ao compartilhar secret entre múltiplos serviços
Se vários serviços compartilham o mesmo secret para assinar tokens, um token emitido para Service A pode sofrer replay no Service B se não verificar aud (audience).
import io.jsonwebtoken.Jwts;
Claims claims = Jwts.parser()
.verifyWith(key)
.requireAudience("order-service") // aceita apenas token destinado a este serviço
.requireIssuer("auth-server")
.build()
.parseSignedClaims(token)
.getPayload();
requireAudience e requireIssuer forçam o parser a lançar exceção se o claim não corresponder — evita que o token seja usado para propósito errado.
10. Integração com Spring Security filter
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
public class JwtAuthFilter extends OncePerRequestFilter {
private final TokenVerifier verifier;
public JwtAuthFilter(TokenVerifier verifier) {
this.verifier = verifier;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
try {
var claims = verifier.verify(header.substring(7));
var auth = new UsernamePasswordAuthenticationToken(
claims.getSubject(), null, List.of());
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (InvalidTokenException e) {
SecurityContextHolder.clearContext();
}
}
chain.doFilter(request, response);
}
}
- Extrai o token do header
Authorization: Bearer <token>. - Verifica com
TokenVerifiercriado na seção 4.3. - Se o token for inválido, limpa o contexto — a requisição será bloqueada pelo Spring Security no próximo passo de autorização, não lança exceção diretamente no filter.
Atenção: Não armazene JWT em
localStoragese o frontend é uma SPA pública — é vulnerável a XSS e roubo de token. Prefira armazenar em cookieHttpOnly,Secure,SameSite=Strictquando possível.
11. Testando o comportamento de verificação de token
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.Test;
import javax.crypto.SecretKey;
import java.time.Instant;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.assertThat;
class TokenVerifierTest {
private final SecretKey key = Keys.secretKeyFor(io.jsonwebtoken.SignatureAlgorithm.HS256);
private final TokenService tokenService = new TokenService(key);
private final TokenVerifier verifier = new TokenVerifier(key);
@Test
void shouldVerifyValidToken() {
String token = tokenService.generateToken("alice");
var claims = verifier.verify(token);
assertThat(claims.getSubject()).isEqualTo("alice");
}
@Test
void shouldRejectTokenSignedWithDifferentKey() {
SecretKey otherKey = Keys.secretKeyFor(io.jsonwebtoken.SignatureAlgorithm.HS256);
TokenService fakeService = new TokenService(otherKey);
String forgedToken = fakeService.generateToken("attacker");
assertThatThrownBy(() -> verifier.verify(forgedToken))
.isInstanceOf(InvalidTokenException.class);
}
}
O segundo teste simula corretamente a situação onde um atacante assina um token com uma chave diferente — o verificador deve rejeitar, nunca aceitar silenciosamente.
shouldVerifyValidToken() PASS
shouldRejectTokenSignedWithDifferentKey() PASS
12. Checklist rápido antes de fazer deploy
- Secret ≥ 256 bits, armazenado em gerenciador de secrets, sem hardcode.
- Whitelist de algoritmos, não confie em
algdo token. - Access token curto (≤ 15 minutos), com refresh token para revogação.
- Não inclua dados sensíveis no payload.
- Valide
exp,iss,audquando necessário. - Armazene token em cookie
HttpOnlyem vez delocalStoragepara SPA. - Tenha mecanismo de revogação/blacklist para refresh token.
13. Conclusão
JWT em si não tem defeito — a maioria dos problemas vem da forma como é implementado: secret fraco, confiança em alg do cliente, token com vida muito longa, ou dados sensíveis no payload público. Aplicando o checklist da seção 12 e sempre usando uma biblioteca JWT bem testada (jjwt, nimbus-jose-jwt) em vez de implementar seu próprio parser, você evita a maioria das vulnerabilidades comuns. O próximo passo a estudar: OAuth 2.0 / OpenID Connect para melhor gerenciar o ciclo de vida de tokens em escala com múltiplos serviços.