Tabla de Contenidos
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-Lengthdel 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óncontent-length-rangeen 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 ensignatureDuration. 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:SourceIpen 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 elGetObjectRequest— 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.
Inicia la conversación