Tokens opacos vs JWT: introspección con Resource Server

Avanzado
Spring Security
Spring Security
Actualizado: 21/04/2026

Cuando una aplicación protege endpoints con OAuth2 Resource Server, hay dos formatos de access token posibles: JWT autoverificable o opaco. La elección impacta latencia, escalabilidad, capacidad de revocación y operativa.

Spring Security moderno soporta los dos modelos con configuración prácticamente idéntica, pero el flujo interno es muy distinto.

JWT vs opaco: la decisión arquitectónica

flowchart LR
    A[Cliente] -->|Bearer token| RS[Resource Server]
    subgraph JWT
        RS -->|Validar firma local| JWK[JWK Set en cache]
    end
    subgraph Opaco
        RS -->|Llamada HTTP| AS[Authorization Server]
        AS -->|active=true / claims| RS
    end

| Aspecto | JWT autoverificable | Token opaco | |---|---|---| | Validación | Local con clave pública | Llamada HTTP a /introspect | | Latencia | Microsegundos | Milisegundos por petición | | Revocación | Imposible antes de expiración | Inmediata | | Tamaño | Centenares de bytes | Decenas de bytes | | Visibilidad de claims | Decodificable por cualquiera | Solo el AS los conoce | | Acoplamiento al AS | Solo en arranque (JWK Set) | En cada petición |

Regla práctica: JWT para microservicios donde la latencia es crítica y se acepta TTL corto (5-15 min). Opacos cuando se necesita revocación inmediata o cuando los claims son sensibles.

Tokens opacos: configuración del Resource Server

spring:
  security:
    oauth2:
      resourceserver:
        opaquetoken:
          introspection-uri: "https://auth.demo.local/oauth2/introspect"
          client-id: "resource-server"
          client-secret: ${RS_SECRET}
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .oauth2ResourceServer(oauth -> oauth.opaqueToken(Customizer.withDefaults()))
        .csrf(csrf -> csrf.disable());
    return http.build();
}

Spring crea un OpaqueTokenIntrospector que llama al endpoint en cada petición protegida. La respuesta del Authorization Server tiene la forma:

{
  "active": true,
  "client_id": "spa-app",
  "username": "ana@demo.local",
  "scope": "tareas:read tareas:write",
  "exp": 1736006400,
  "sub": "user-789"
}

Si active=false, Spring rechaza la petición con 401 Unauthorized.

Cache para mitigar latencia

Llamar al introspection endpoint en cada petición es prohibitivo. La solución es cachear la respuesta hasta que expire el token o se invalide.

@Bean
OpaqueTokenIntrospector cachedIntrospector(
        @Value("${spring.security.oauth2.resourceserver.opaquetoken.introspection-uri}") String uri,
        @Value("${spring.security.oauth2.resourceserver.opaquetoken.client-id}") String clientId,
        @Value("${spring.security.oauth2.resourceserver.opaquetoken.client-secret}") String clientSecret) {

    SpringOpaqueTokenIntrospector delegate = new SpringOpaqueTokenIntrospector(uri, clientId, clientSecret);

    return token -> {
        OAuth2AuthenticatedPrincipal cached = cache.get(token);
        if (cached != null) return cached;
        OAuth2AuthenticatedPrincipal principal = delegate.introspect(token);
        long ttl = Duration.between(Instant.now(),
            Instant.ofEpochSecond((Long) principal.getAttribute("exp"))).getSeconds();
        cache.put(token, principal, Math.min(ttl, 300));
        return principal;
    };
}

Cachear como máximo 5 minutos equilibra rendimiento y latencia de revocación. Para invalidación inmediata, integra el cache con eventos del Authorization Server (Kafka, Redis pub/sub).

El token mismo sirve como clave de cache. No lo loguees ni lo persistas en disco.

Personalizar el AuthenticationConverter

Por defecto, Spring mapea el scope del response a autoridades SCOPE_*. Para mapear roles custom, sobrescribe el converter.

@Bean
OpaqueTokenAuthenticationConverter rolesConverter() {
    return (introspectedToken, principal) -> {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        Object scope = principal.getAttribute("scope");
        if (scope instanceof String s) {
            Arrays.stream(s.split(" "))
                .map(sc -> new SimpleGrantedAuthority("SCOPE_" + sc))
                .forEach(authorities::add);
        }
        Object roles = principal.getAttribute("roles");
        if (roles instanceof Collection<?> r) {
            r.forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
        }
        return new BearerTokenAuthentication(principal, ((OAuth2IntrospectionAuthenticatedPrincipal) principal).getAttributes(), authorities);
    };
}

Hybrid: JWT + introspección

En arquitecturas avanzadas se combina lo mejor de ambos: el token es JWT autoverificable para velocidad, pero ante endpoints de alto valor se llama al introspection endpoint para confirmar que no fue revocado.

@PreAuthorize("hasAuthority('SCOPE_pagos:write')")
@PostMapping("/pagos")
public Pago crear(@RequestBody PagoCommand cmd, @AuthenticationPrincipal Jwt jwt) {
    if (!introspector.isActive(jwt.getTokenValue())) {
        throw new AuthenticationCredentialsNotFoundException("Token revocado");
    }
    return service.crear(cmd);
}

Este patrón añade latencia solo a las operaciones críticas, manteniendo el resto a velocidad JWT.

Revocación con tokens opacos

El RFC 7009 define el endpoint /oauth2/revoke para invalidar tokens. El cliente envía el token a revocar y el Authorization Server lo marca como inactivo.

POST /oauth2/revoke HTTP/1.1
Host: auth.demo.local
Authorization: Basic c3BhLWNsaWVudDpzZWNyZXQ=
Content-Type: application/x-www-form-urlencoded

token=opaque_abc123&token_type_hint=access_token

A partir de la siguiente petición a /introspect, el Authorization Server responde active=false.

Para revocar un JWT, la única opción es mantener una denylist de tokens revocados consultada en cada validación. Es un anti-patrón que devuelve la latencia que el JWT pretendía ahorrar.

Cuando elegir cada formato

Elige JWT autoverificable cuando:

  • Tienes muchos microservicios y la latencia es crítica.
  • Los TTLs cortos (5-15 min) son aceptables.
  • Los claims públicos no exponen información sensible.
  • La ratio de revocaciones es baja.

Elige opaco cuando:

  • Necesitas revocar tokens al instante (cierre de sesión, cambio de contraseña).
  • Los claims contienen información que no debe ser visible para el cliente.
  • El Authorization Server tiene baja latencia y alta disponibilidad.
  • La ratio de peticiones por usuario es moderada.

Patrones de migración

Si una API existente usa JWT y se quiere migrar a opacos sin downtime:

  • 1. Configura el Resource Server con ambos: JWT y opaco a la vez.
  • 2. El Authorization Server emite los dos formatos según el cliente o el endpoint.
  • 3. Migra clientes gradualmente usando opacos.
  • 4. Cuando todos los clientes hayan migrado, deshabilita JWT.
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .oauth2ResourceServer(oauth -> oauth
            .jwt(Customizer.withDefaults())
            .opaqueToken(Customizer.withDefaults()));
    return http.build();
}

Spring detecta el formato del token (header tipo JWT vs string opaco) y aplica el verificador adecuado.

Errores frecuentes

  • No cachear introspección: añade decenas de milisegundos a cada petición y satura el AS.
  • TTL del cache mayor que el del token: el cache devuelve "active" cuando ya expiró.
  • Compartir el client_secret del Resource Server: cada microservicio debería tener su propio par credentials.
  • Usar JWT con alg: none: clásico bypass. Restringe jwsAlgorithms a RS256 o ES256.
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 Security 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 Security

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

Aprendizajes de esta lección

Comparar tokens JWT autoverificables con tokens opacos validados por introspección, configurar Spring Resource Server con OpaqueTokenIntrospector y elegir según el caso.