JWT con refresh tokens en Spring Security

Intermedio
Spring Boot
Spring Boot
Actualizado: 04/05/2026

Diagrama: tutorial-spring-security-jwt-refresh-tokens

Por qué separar access token y refresh token

Usar un único JWT de larga duración es peligroso: si se filtra, el atacante mantiene el acceso hasta que el token expire, que pueden ser horas o días. Si se usa uno muy corto, el usuario tiene que reautenticarse constantemente y la experiencia sufre. La solución consiste en combinar dos tokens con roles distintos:

  • Access token: JWT de vida corta (entre 5 y 15 minutos). Viaja en la cabecera Authorization en cada petición a la API y se valida en local sin acceder a base de datos.
  • Refresh token: token opaco o JWT de vida larga (días o semanas). Solo se envía al endpoint de renovación y se valida contra la base de datos.

El cliente obtiene ambos al autenticarse. Cuando el access token expira, usa el refresh token para pedir uno nuevo sin que el usuario tenga que volver a introducir credenciales.

El access token viaja por toda la API y debe poder validarse sin consultar base de datos (claves públicas RS256 en JWK Set). El refresh token solo viaja al endpoint de renovación, donde sí se permite consultar la base de datos para validarlo.

Modelo de datos para refresh tokens

El refresh token se persiste con suficiente metadata para auditoría, rotación y detección de robo. Un diseño habitual incluye:

import jakarta.persistence.*;
import java.time.Instant;
import java.util.UUID;

@Entity
@Table(name = "refresh_tokens")
public class RefreshToken {

    @Id
    @GeneratedValue
    private UUID id;

    @Column(nullable = false, unique = true, length = 512)
    private String token;

    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    private User user;

    @Column(nullable = false)
    private Instant issuedAt;

    @Column(nullable = false)
    private Instant expiresAt;

    private Instant usedAt;

    @Column(length = 256)
    private String replacedBy;

    private boolean revoked;

    // getters y setters
}

El campo replacedBy apunta al siguiente token de la familia: cada rotación genera un token nuevo y marca el anterior como usado enlazándolo con el que lo sustituye. Esta cadena permite detectar intentos de reutilización.

Endpoint de refresh con rotación

El endpoint /auth/refresh recibe el refresh token, valida que no esté revocado ni expirado, lo rota y emite un nuevo par de tokens. La rotación es una medida crítica: cada refresh token es de un solo uso.

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
public class AuthController {

    private final JwtService jwtService;
    private final RefreshTokenService refreshTokenService;

    public AuthController(JwtService jwtService, RefreshTokenService refreshTokenService) {
        this.jwtService = jwtService;
        this.refreshTokenService = refreshTokenService;
    }

    @PostMapping("/refresh")
    public TokenResponse refresh(@RequestBody RefreshRequest request) {
        RefreshToken stored = refreshTokenService.verify(request.refreshToken());
        User user = stored.getUser();

        String accessToken = jwtService.createAccessToken(user);
        RefreshToken newRefresh = refreshTokenService.rotate(stored, user);

        return new TokenResponse(accessToken, newRefresh.getToken());
    }
}

El método rotate del servicio marca el token anterior como usado y crea uno nuevo enlazado por replacedBy. Si más tarde llega otra petición con el mismo refresh, significa que alguien lo ha copiado: el sistema debe asumir compromiso y revocar toda la familia de tokens de ese usuario, obligándole a autenticarse de nuevo.

public RefreshToken rotate(RefreshToken old, User user) {
    if (old.getUsedAt() != null) {
        revocarFamilia(old);
        throw new TokenReplayException("Refresh token reutilizado. Sesion revocada.");
    }
    old.setUsedAt(Instant.now());
    RefreshToken nuevo = new RefreshToken(generarTokenOpaco(), user);
    old.setReplacedBy(nuevo.getToken());
    repository.save(old);
    return repository.save(nuevo);
}

La detección de reutilización es una de las defensas más efectivas contra el robo de refresh tokens. El cliente legítimo nunca reutiliza un refresh porque al rotarlo guarda el nuevo; solo un atacante con un token antiguo intentaría usar uno ya consumido.

Revocación centralizada y cookies seguras

Cuando un usuario cierra sesión desde uno de sus dispositivos o el equipo de soporte debe cortar el acceso de una cuenta comprometida, basta con marcar sus refresh tokens como revoked en base de datos. El access token activo seguirá funcionando hasta expirar (5 a 15 minutos), pero no se podrá renovar más.

Para aplicaciones web, almacenar el refresh token en una cookie HttpOnly, Secure y SameSite=Strict reduce drásticamente el riesgo frente a XSS. El navegador la enviará automáticamente al endpoint /auth/refresh y el JavaScript de la página no podrá leerla.

Buenas prácticas recomendadas en 2026:

  • Firma los JWT con claves asimétricas (RS256 o ES256) para que los microservicios verifiquen sin conocer la clave privada.
  • Incluye claims estándar (iss, aud, sub, exp, iat, jti) y evita meter datos sensibles en el payload.
  • Registra cada refresh exitoso y cada revocación en un log de auditoría con IP y user agent.
  • Monitoriza intentos repetidos con refresh tokens inválidos: son indicio de ataque activo.

Caso B2B: banca corporativa con sesiones auditables

Un banco corporativo español despliega una banca electrónica para tesoreros de grandes empresas. Cada sesión usa access tokens de 10 minutos firmados con ES256 y refresh tokens rotatorios de 4 horas persistidos en PostgreSQL con campos de IP y dispositivo. Cuando el sistema antifraude detecta un refresh reutilizado desde una IP distinta, marca toda la familia como revocada y notifica al tesorero por email en segundos. El equipo de cumplimiento normativo auditó el diseño frente a los requisitos PSD2 y DORA y lo integró en el programa de certificación ISO 27001, reduciendo en un 90 por ciento los incidentes de sesiones colgadas tras cambios de dispositivo.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Boot es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Spring Boot

Explora más contenido relacionado con Spring Boot y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Diseñar un flujo con access y refresh token, implementar rotación, detectar reutilización de refresh como indicio de robo y revocar sesiones de forma centralizada.