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.

Create a free account to save this page, take notes and track your progress. Sign in

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

Was this page helpful?