AWS S3 Presigned URLs: Cómo Generar y Usar URLs Firmadas con Java (SDK v2)

Tabla de Contenidos

  1. Generando un Presigned URL de Descarga (GET)
  2. Generando un Presigned URL de Subida (PUT)
  3. ¿Cuánto Dura un Presigned URL?
  4. El Gap de Presigned POST en Java
  5. Migrando de SDK v1 a SDK v2
  6. Seguridad: Más Allá de la Expiración
  7. Mi Evaluación
  8. Recursos Oficiales 📚

AWS S3 Presigned URLs: Cómo Generar y Usar URLs Firmadas con Java (SDK v2)

Actualizado en julio 2026: la versión original de este artículo (noviembre 2021) usaba AWS SDK for Java v1. Ese SDK entró en maintenance mode el 31 de julio de 2024 y llegó a end-of-support el 31 de diciembre de 2025 — ya no recibe parches ni actualizaciones. Reescribí el artículo completo con SDK v2 (S3Presigner), y sumé varios gotchas de duración y seguridad que la versión original no cubría.

En S3, todos los objetos son privados por defecto. Un presigned URL es la forma en que el dueño de un objeto le da acceso temporal a alguien más para subirlo o descargarlo — sin compartir credenciales, sin tocar permisos de IAM, solo un link con fecha de caducidad.

Es una de esas primitivas que parece trivial hasta que la llevas a producción: la duración real depende del tipo de credencial que firmó el URL (no solo del parámetro que configuraste), un PUT presignado no te deja limitar el tamaño del archivo que alguien sube, y un URL filtrado sigue siendo válido hasta que revocas la credencial que lo generó — no existe un botón de “invalidar” individual.

Vamos a construirlo con AWS SDK v2 sobre un endpoint Quarkus, y a cubrir los detalles que sí importan cuando esto corre en producción.

🎯 ProTip #1: Un presigned URL es, para todo efecto práctico, un token al portador. Cualquiera con el link puede usarlo — no hay verificación de identidad más allá de la firma criptográfica. Si necesitas revocarlo antes de su expiración, la única forma es rotar o desactivar las credenciales que lo firmaron.

Generando un Presigned URL de Descarga (GET)

Con SDK v2, la clase que genera URLs firmados es S3Presigner — no el S3Client directamente:

import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
import java.time.Duration;

@Path("/files")
public class FileResource {

    @GET
    @Path("/{bucket}/{key}/download-url")
    public Response getDownloadUrl(@PathParam("bucket") String bucket,
                                    @PathParam("key") String key) {

        try (S3Presigner presigner = S3Presigner.create()) {
            GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                    .bucket(bucket)
                    .key(key)
                    .build();

            GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
                    .signatureDuration(Duration.ofMinutes(15))
                    .getObjectRequest(getObjectRequest)
                    .build();

            PresignedGetObjectRequest presignedRequest = presigner.presignGetObject(presignRequest);

            return Response.ok(presignedRequest.url().toString()).build();
        }
    }
}

S3Presigner implementa AutoCloseable — ciérralo (o reutiliza una instancia singleton vía CDI) en vez de crear uno por request si tu servicio tiene tráfico real.

Generando un Presigned URL de Subida (PUT)

El flujo es simétrico, cambiando GetObjectRequest por PutObjectRequest:

import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
import java.util.Map;

@POST
@Path("/{bucket}/{key}/upload-url")
public Response getUploadUrl(@PathParam("bucket") String bucket,
                              @PathParam("key") String key) {

    try (S3Presigner presigner = S3Presigner.create()) {
        PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                .bucket(bucket)
                .key(key)
                .contentType("image/jpeg")
                .metadata(Map.of("title", key))
                .build();

        PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
                .signatureDuration(Duration.ofMinutes(15))
                .putObjectRequest(putObjectRequest)
                .build();

        PresignedPutObjectRequest presignedRequest = presigner.presignPutObject(presignRequest);

        return Response.ok(presignedRequest.url().toString()).build();
    }
}

Quien reciba este URL sube el archivo con una simple petición PUT — sin SDK, sin credenciales de AWS, solo un HttpClient cualquiera:

HttpClient httpClient = HttpClient.newHttpClient();
httpClient.send(HttpRequest.newBuilder()
        .uri(new URL(presignedUrlString).toURI())
        .PUT(HttpRequest.BodyPublishers.ofFile(Path.of("foto.jpg")))
        .build(), HttpResponse.BodyHandlers.discarding());

⚠️ ProTip #2: Un presigned PUT no te permite limitar el tamaño del archivo que el cliente sube — S3 no valida el Content-Length del lado del servidor aunque lo firmes como header. El límite real es el máximo de un PUT simple en S3: 5 GB. Si necesitas un límite real (5 MB para avatares, por ejemplo), la herramienta correcta es un Presigned POST con la condición content-length-range en la policy — no un PUT. Con un matiz importante para Java: sigue abajo.

¿Cuánto Dura un Presigned URL?

El SDK te deja fijar hasta 7 días de duración usando AWS Signature Version 4 (desde la consola de S3, el máximo es 12 horas). Pero esa duración que configuras es solo un techo — no una garantía. Si el URL se firmó con credenciales temporales, deja de funcionar en cuanto esas credenciales expiran, sin importar cuánto le hayas puesto:

Tipo de credencial Qué determina la expiración real
IAM User (larga duración) La duración que fijaste en el request, hasta 7 días
IAM Role / STS AssumeRole Termina cuando expira la sesión del rol — aunque el URL diga que dura más
Instance profile (EC2, Lambda, ECS) Termina cuando expiran las credenciales temporales de la instancia o función
S3 Express (directory buckets) Máximo 5 minutos — las credenciales de S3 Express viven solo 5 minutos

🔍 ProTip #3: Este es el gotcha que más rompe presigned URLs en producción. Generas el URL con Duration.ofDays(1) desde una función Lambda, pero el rol de ejecución de esa Lambda usa credenciales temporales que expiran en minutos u horas. El URL “vive” hasta esa expiración real, no hasta la que configuraste en signatureDuration. Verifica siempre qué tipo de credencial está firmando, no solo el parámetro del request.

El Gap de Presigned POST en Java

Si necesitas de verdad limitar el tamaño del archivo subido, la herramienta es un Presigned POST: en vez de firmar una sola URL, firmas una policy con condiciones —incluyendo content-length-range— y el cliente sube el archivo vía un formulario multipart/form-data.

El problema: AWS SDK for Java v2 todavía no tiene soporte nativo para generar presigned POST, a diferencia de boto3 (Python), el SDK de JavaScript o el de Go. Es un feature request abierto desde 2019 sin resolver a julio de 2026. Las salidas prácticas hoy son: implementar la firma de la policy a mano (no trivial, pero documentado), delegar ese endpoint específico a una función Lambda en Python o Node.js, o aceptar el límite de PUT y validar el tamaño en tu capa de aplicación después de la subida.

Migrando de SDK v1 a SDK v2

Si todavía tienes código como el de la versión original de este artículo (GeneratePresignedUrlRequest, TransferManagerBuilder), esta es la tabla de migración:

Concepto SDK v1 (EOL desde dic. 2025) SDK v2
Cliente AmazonS3 / TransferManager S3Client / S3TransferManager
Generar URL s3Client.generatePresignedUrl(...) presigner.presignGetObject(...) / presignPutObject(...)
Request de firma GeneratePresignedUrlRequest GetObjectPresignRequest / PutObjectPresignRequest
Duración .withExpiration(Date) .signatureDuration(Duration)
Paquete base com.amazonaws.* software.amazon.awssdk.*
Excepciones AmazonServiceException S3Exception (extiende AwsServiceException)

Seguridad: Más Allá de la Expiración

  • Es un bearer token. Cualquiera con el link puede usarlo. No hay verificación de identidad adicional — la seguridad depende completamente de que el link no se filtre y de la duración que configuraste.
  • Restringe por IP si el caso lo permite. Una condición aws:SourceIp en la policy de IAM o del bucket limita desde qué rangos de IP es válido el presigned URL, incluso si el link se filtra fuera de esos rangos.
  • Sanitiza el object key. Si el key viene de input del usuario, rechaza secuencias como ../ antes de construir el GetObjectRequest — sin esa validación, un key malicioso puede intentar acceder fuera del prefijo esperado.
  • No hay revocación individual. Si un presigned URL se filtró y necesitas invalidarlo antes de su expiración natural, la única palanca real es rotar o desactivar las credenciales que lo firmaron — lo que probablemente invalide también otros URLs generados con esa misma credencial.

Mi Evaluación

Los presigned URLs siguen siendo, cinco años después de la versión original de este artículo, la forma correcta de dar acceso temporal a un objeto de S3 sin exponer credenciales — eso no cambió. Lo que cambió es la superficie de API (SDK v1 está muerto) y lo que yo mismo no cubrí la primera vez: la duración real no es la que configuras, sino la que permite la credencial que firma; y si necesitas límite de tamaño real en la subida, PUT no es la herramienta — y en Java, ni siquiera POST lo es todavía de forma nativa.

¿Estás migrando código de SDK v1, o implementando presigned URLs por primera vez? Cuéntame qué gotcha te encontraste — los comentarios están abiertos.


Recursos Oficiales 📚

¿Tienes un proyecto en AWS?

Conversemos sobre tu arquitectura. Tengo experiencia en soluciones cloud-native, IA generativa y modernización de aplicaciones.

Agendemos una llamada →

o ver detalles de los servicios primero

Escrito por

Gerardo Arroyo Arce

Arquitecto de Soluciones, AWS Golden Jacket con pasión por compartir conocimiento. Como miembro activo de AWS Community Builders, ex-AWS Ambassador y AWS User Group Leader, me dedico a construir puentes entre la tecnología y las personas. Desarrollador Java de corazón y consultor independiente, llevo la arquitectura cloud más allá de la teoría a través de conferencias internacionales y soluciones del mundo real. Mi curiosidad insaciable por aprender y compartir me mantiene en constante evolución junto a la comunidad tech.

Inicia la conversación