Spring Boot cookbook · Intermediate

Upload files to S3 from Spring Boot with presigned URLs

Let browsers upload large files directly to S3 while your API controls who can upload what.

Approach

Use the AWS SDK v2 S3Presigner to generate a short-lived presigned PUT URL for a server-generated key and content type. The client uploads directly to S3, then calls your API to confirm; the API verifies the object with headObject before saving metadata.

Solution

@RestController
@RequestMapping("/api/v1/uploads")
class UploadController {
    private static final Set<String> ALLOWED = Set.of("image/png", "image/jpeg", "application/pdf");
    private final S3Presigner presigner;
    private final S3Client s3;
    private final String bucket;

    UploadController(S3Presigner presigner, S3Client s3, @Value("${app.uploads.bucket}") String bucket) {
        this.presigner = presigner; this.s3 = s3; this.bucket = bucket;
    }

    record UploadRequest(@NotBlank String contentType, @Positive @Max(52_428_800) long size) {}
    record UploadResponse(String key, String url) {}

    @PostMapping
    UploadResponse create(@Valid @RequestBody UploadRequest req, Authentication auth) {
        if (!ALLOWED.contains(req.contentType())) throw new ResponseStatusException(HttpStatus.UNSUPPORTED_MEDIA_TYPE);
        String key = "users/%s/%s".formatted(auth.getName(), UUID.randomUUID());
        PutObjectRequest put = PutObjectRequest.builder().bucket(bucket).key(key)
                .contentType(req.contentType()).contentLength(req.size()).build();
        PresignedPutObjectRequest presigned = presigner.presignPutObject(p -> p
                .signatureDuration(Duration.ofMinutes(5)).putObjectRequest(put));
        return new UploadResponse(key, presigned.url().toString());
    }

    @PostMapping("/complete")
    ResponseEntity<Void> complete(@RequestParam String key, Authentication auth) {
        if (!key.startsWith("users/" + auth.getName() + "/")) throw new ResponseStatusException(HttpStatus.FORBIDDEN);
        HeadObjectResponse head = s3.headObject(h -> h.bucket(bucket).key(key));   // throws if missing
        files.register(auth.getName(), key, head.contentLength(), head.contentType());
        return ResponseEntity.noContent().build();
    }
}

Pitfalls

  • Accepting client-chosen object keys.
  • Long-lived presigned URLs.
  • Streaming large uploads through the app (MultipartFile) when direct upload is possible.

Variations

  • Spring Cloud AWS S3Template simplifies presigning.
  • S3 event notifications to trigger scanning/thumbnails.

Further reading: AWS SDK for Java: Presigned URLs

Reference: https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/examples-s3-presign.html

More Spring Boot recipes