Refresh token no Spring Security: por que o logout pode falhar

se o seu refresh token no Spring Security continua valendo depois do logout, depois de uma troca de senha ou depois de uma suspeita de vazamento, a porta para invasão já está meio aberta. o problema quase nunca é o JWT em si; é a falta de rotação de token e de revogação de JWT no backend, que transforma um refresh token roubado em acesso prolongado à API. Se a base desse fluxo ainda nao estiver redonda, vale revisar O que é o Spring Boot e para que serve? antes de avancar.

Em produção, esse erro aparece com cara de “funciona normalmente” até o dia em que alguém descobre que a sessão não morre de verdade. a correção passa por três decisões simples de entender e difíceis de ignorar: rotação, revogação e rastreabilidade de uso. Para aprofundar essa decisao sem criar outra URL concorrente, o melhor complemento aqui e JWT Spring Boot Java: Guia Completo com Exemplo Prático.

spring security refresh token rotacao logout revogacao: onde a maioria erra

O padrão ruim é conhecido: o usuário faz login, recebe access token curto e refresh token longo, a aplicação renova o access token quando ele expira e pronto. se o refresh token nunca muda, nunca é invalidado e nunca é checado no servidor, ele vira uma credencial de longa duração. isso parece prático até um token vazar por log, backup, dispositivo comprometido ou extensão maliciosa no navegador. Esse ponto fica mais claro quando voce conecta com Guia de Java para backend: fundamentos, Spring e próximos passos.

Quando falamos de refresh token Spring Security, a parte crítica não é só emitir token. é manter um controle mínimo para saber se aquele token ainda é legítimo, se já foi usado, se foi trocado por outro e se a sessão ainda existe. sem isso, o sistema depende só da boa vontade da expiração. Para aprofundar essa decisao sem criar outra URL concorrente, o melhor complemento aqui e JWT no Spring Security com Spring Boot: autenticação moderna passo a passo.

O erro mais caro: confiar em expiração como se fosse revogação

Expiração não é revogação. esse é o detalhe que derruba muita implementação. um refresh token com validade de 30 dias pode continuar útil durante todo esse tempo mesmo depois de logout na interface, desde que o backend não o invalidar explicitamente. Na prática, em APIs de produção, isso é um convite para abuso persistente. Depois de ajustar esse trecho, o proximo passo natural e seguir para Guia de Spring Security com JWT: autenticação sem dor.

Um cenário real é o de suporte interno ou portal administrativo com acesso por navegador compartilhado. o usuário clica em sair, acha que encerrou a sessão e fecha a aba. se o refresh token ficou no storage, cookies ou banco sem revogação, qualquer pessoa com acesso posterior ao dispositivo ainda pode renovar a autenticação.

Spring security refresh token rotacao logout revogacao: rotacao de token: o que muda de verdade no fluxo

A rotação de token resolve um problema bem específico: mesmo que um refresh token seja interceptado, ele perde valor depois do primeiro uso. ao renovar a sessão, o backend entrega um novo refresh token e invalida o anterior. se o token antigo aparecer de novo, isso é sinal forte de replay ou roubo.

A abordagem melhor é essa. a pior é emitir o mesmo refresh token por semanas e só trocar o access token. a diferença não é teórica. Na prática, na primeira opção, o vazamento tem vida curta. Ainda assim, na segunda, o invasor ganha uma credencial estável para continuar acessando a API sem chamar atenção.

Outro ganho prático da rotação é o rastreamento de sessão. você consegue identificar a família de tokens, invalidar tudo em lote e reagir melhor a logout, troca de senha, suspeita de fraude ou desativação de conta.

Quando rotação vira obrigação, não luxo

Se a sua aplicação tem painel administrativo, acesso financeiro, dados sensíveis ou múltiplos dispositivos por usuário, rotação de token deixa de ser opcional. em banco digital, ERP, saúde, logística e até SaaS B2B com permissões granulares, refresh token sem rotação é uma aposta ruim. o vazamento de um único token pode sustentar acesso por dias ou semanas.

Em apps simples, com baixo risco e curto tempo de sessão, muita equipe tenta evitar estado no servidor. Ainda assim, a rotação costuma compensar porque reduz a janela de abuso sem exigir um sistema pesado. o custo extra é pequeno perto do custo de investigar incidente.

erros comuns de refresh token spring security em produção

O primeiro erro é tratar refresh token como se fosse um access token longo. o segundo é usar o mesmo identificador para sempre. o terceiro é não diferenciar logout de expiração. Na prática, esses três juntos criam o cenário perfeito para invasão silenciosa.

Em produção, os sintomas aparecem de forma estranha: o usuário diz que saiu da conta, mas o navegador continua renovando sessão; um dispositivo antigo mantém acesso depois de troca de senha; ou duas sessões concorrentes seguem vivas mesmo após um dos refresh tokens ser supostamente substituído. quando isso acontece, o problema raramente está na assinatura JWT. o buraco está na revogação e no controle de uso.

Outro erro comum de rotação de token é salvar o refresh token em texto puro sem necessidade. se o banco de dados vaza, o invasor ganha acesso pronto. melhor guardar hash do token ou um identificador mínimo que permita validação e revogação sem expor a credencial original.

Bloco de erro comum: causa, sintoma e correção

Causa: o backend emite refresh token novo, mas não invalida o anterior. Sintoma: o mesmo refresh token funciona mais de uma vez e renova sessões paralelas. Correção: ao receber um refresh token válido, marque-o como usado, emita outro e invalide toda a família se o token antigo reaparecer.

Esse tipo de falha é perigoso porque parece estabilidade. em vez de quebrar, a API continua respondendo. só que ela está permitindo replay. Na prática, em segurança, esse é o tipo de problema que passa no teste funcional e falha no mundo real.

secao pratica com codigo completo: um fluxo seguro de refresh token

Para o exemplo ficar direto ao ponto, a ideia abaixo usa um modelo simples de persistência de sessão. você pode adaptar para banco relacional, Redis ou outra store. o importante é separar access token de refresh token, armazenar referência do refresh token e controlar status de uso.

O fluxo tem quatro etapas: login, refresh, logout e detecção de reutilização. a implementação abaixo não tenta ser “enterprise”, mas mostra o esqueleto certo para produção.

Modelo de sessão

Limites do exemplo: o código a seguir é didático e não está pronto para produção. A emissão de access token é um placeholder, o repositório é em memória e faltam a classe UnauthorizedException, imports e integração HTTP. O exemplo também não implementa revogação da família de tokens nem consumo atômico. Para produção, use persistência compartilhada, controle transacional ou compare-and-set para impedir dois refreshes simultâneos, identificação da família e revogação persistida ao detectar reutilização. Revogar o refresh token não invalida automaticamente access tokens já emitidos.

public class RefreshSession {
    private String id;
    private String userId;
    private String tokenHash;
    private boolean revoked;
    private boolean used;
    private Instant expiresAt;

    public RefreshSession(String id, String userId, String tokenHash, Instant expiresAt) {
        this.id = id;
        this.userId = userId;
        this.tokenHash = tokenHash;
        this.expiresAt = expiresAt;
    }

    public boolean isActive() {
        return !revoked && !used && expiresAt.isAfter(Instant.now());
    }

    public void revoke() {
        this.revoked = true;
    }

    public void markUsed() {
        this.used = true;
    }

    public String getId() { return id; }
    public String getUserId() { return userId; }
    public String getTokenHash() { return tokenHash; }
    public boolean isRevoked() { return revoked; }
    public boolean isUsed() { return used; }
    public Instant getExpiresAt() { return expiresAt; }
}

Serviço de tokens com rotação e revogação

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.Base64;
import java.util.HexFormat;
import java.util.Optional;
import java.util.UUID;

public class TokenService {
    private final RefreshSessionRepository repository;
    private final SecureRandom secureRandom = new SecureRandom();

    public TokenService(RefreshSessionRepository repository) {
        this.repository = repository;
    }

    public TokenPair login(String userId) {
        String accessToken = generateAccessToken(userId);
        String rawRefreshToken = generateRawRefreshToken();
        String tokenHash = hash(rawRefreshToken);

        RefreshSession session = new RefreshSession(
                UUID.randomUUID().toString(),
                userId,
                tokenHash,
                Instant.now().plusSeconds(30L * 24 * 3600)
        );

        repository.save(session);
        return new TokenPair(accessToken, rawRefreshToken);
    }

    public TokenPair refresh(String rawRefreshToken) {
        String tokenHash = hash(rawRefreshToken);
        RefreshSession session = repository.findByTokenHash(tokenHash)
                .orElseThrow(() -> new UnauthorizedException("refresh_token_invalido"));

        if (!session.isActive()) {
            throw new UnauthorizedException("refresh_token_revogado_ou_expirado");
        }

        session.markUsed();
        repository.save(session);

        String newAccessToken = generateAccessToken(session.getUserId());
        String newRawRefreshToken = generateRawRefreshToken();
        String newHash = hash(newRawRefreshToken);

        RefreshSession newSession = new RefreshSession(
                UUID.randomUUID().toString(),
                session.getUserId(),
                newHash,
                Instant.now().plusSeconds(30L * 24 * 3600)
        );

        repository.save(newSession);
        return new TokenPair(newAccessToken, newRawRefreshToken);
    }

    public void logout(String rawRefreshToken) {
        String tokenHash = hash(rawRefreshToken);
        repository.findByTokenHash(tokenHash).ifPresent(session -> {
            session.revoke();
            repository.save(session);
        });
    }

    private String generateAccessToken(String userId) {
        return "access-" + userId + "-" + UUID.randomUUID();
    }

    private String generateRawRefreshToken() {
        byte[] bytes = new byte[48];
        secureRandom.nextBytes(bytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }

    private String hash(String value) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(value.getBytes(StandardCharsets.UTF_8));
            return HexFormat.of().formatHex(hash);
        } catch (Exception e) {
            throw new IllegalStateException("erro_hash_refresh_token", e);
        }
    }

    public record TokenPair(String accessToken, String refreshToken) {}
}

Repositório mínimo

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

public class InMemoryRefreshSessionRepository implements RefreshSessionRepository {
    private final Map<String, RefreshSession> store = new ConcurrentHashMap<>();

    @Override
    public void save(RefreshSession session) {
        store.put(session.getTokenHash(), session);
    }

    @Override
    public Optional<RefreshSession> findByTokenHash(String tokenHash) {
        return Optional.ofNullable(store.get(tokenHash));
    }
}

interface RefreshSessionRepository {
    void save(RefreshSession session);
    Optional<RefreshSession> findByTokenHash(String tokenHash);
}

Esse código deixa claro o ponto central: refresh token não é só assinar e devolver. ele precisa ser rastreável, revogável e descartável após o uso. se o mesmo token reaparecer, o sistema já tem informação suficiente para derrubar a sessão e pedir novo login.

Integração com Spring Security sem complicar demais

No Spring Security, a lógica de autenticação continua no filtro ou no endpoint de refresh, mas a decisão de segurança fica no serviço de token. isso ajuda a separar responsabilidades. o filtro valida access token. Na prática, o endpoint de refresh valida o refresh token e aplica rotação. Ainda assim, logout marca revogação. Por isso, simples, mas com fronteira clara.

Se você já domina a base de autenticação e quer contextualizar a stack, faz sentido revisar primeiro O que é o Spring Boot e para que serve? e depois aprofundar no ecossistema com Guia de Java para backend: fundamentos, Spring e próximos passos. para o lado de JWT, os próximos passos naturais são JWT no Spring Security com Spring Boot: autenticação moderna passo a passo, JWT Spring Boot Java: Guia Completo com Exemplo Prático e Guia de Spring Security com JWT: autenticação sem dor.

quando usar e quando evitar refresh token com rotação

Use rotação e revogação quando a autenticação precisa sobreviver a risco real. se a API atende app mobile, portal corporativo, painel administrativo ou qualquer fluxo com dados sensíveis, a disciplina vale muito a pena. em dispositivos móveis, por exemplo, o refresh token costuma viver por mais tempo, então a janela de ataque aumenta. Na prática, em sistemas com múltiplos dispositivos, a rotação ajuda a separar sessões e encerrar somente o que foi comprometido.

Evite complicar demais se o sistema for muito pequeno, sem login persistente e com sessão curta de baixo impacto. Ainda assim, mesmo nesses casos, eu seria conservador: access token curto e refresh token com revogação mínima dão muito mais segurança do que uma solução “stateless” que só parece elegante no desenho da arquitetura.

O trade-off é claro. quanto mais controle você quer, mais estado precisa manter. quanto menos controle, maior o risco de replay e permanência indevida. Na prática, em segurança de API, esse custo adicional costuma ser barato perto do custo operacional de um incidente.

Spring security refresh token rotacao logout revogacao: referencias externas

Para validar detalhes de implementacao e aprofundar a configuracao, vale consultar a documentacao oficial do Spring Security, o guia de claims no JWT.io e a documentacao do Spring Boot.

FAQ

Como fazer refresh token no Spring Security com rotação?

Emita um novo refresh token a cada uso, invalide o anterior no backend e associe cada token a uma sessão rastreável. se o token antigo aparecer de novo, trate como reutilização suspeita e revogue a família inteira.

Preciso revogar refresh token quando o usuário faz logout?

Sim. logout sem revogação é só limpeza de interface. se o refresh token continuar válido no servidor, a sessão ainda pode ser renovada mesmo depois da saída visual do usuário.

Qual a diferença entre rotação e revogação de JWT no Spring Security?

Rotação substitui o refresh token usado por outro novo. revogação invalida um token ou uma sessão inteira antes do vencimento. as duas coisas se complementam: rotação reduz reutilização, revogação encerra acesso quando algo sai do controle.

Conclusão e próximos passos

Refresh token no Spring Security não é só uma peça de autenticação. é uma decisão de segurança de API. se você não rotaciona, não revoga e não rastreia uso, está aceitando que um token roubado continue útil por muito mais tempo do que deveria. Na prática, em produção, isso é o tipo de falha que passa despercebida até virar incidente.

O caminho mais seguro é direto: access token curto, refresh token com rotação, revogação no logout e detecção de reutilização. se a sua aplicação já usa JWT, o próximo passo não é criar um fluxo mais sofisticado. é fechar a brecha que mais costuma aparecer em sistemas que “funcionam” mas não se defendem bem.

Leitura complementar: Guia de Spring Security com JWT: autenticação sem dor, JWT no Spring Security com Spring Boot: autenticação moderna passo a passo e JWT Spring Boot Java: Guia Completo com Exemplo Prático. os proximos passos sao validar esse fluxo no seu projeto, ajustar o caso de uso real e cobrir a implementacao com testes.

Deixe um comentário