AWS
Spring Boot with Amazon S3: Upload, Download and Secure Files
By Utility Zone ยท 2026-10-03T15:48:27.050542
A practical guide for Java developers integrating a Spring Boot REST API with Amazon S3.
Before production: Treat the code as a starting template. Verify Spring Boot, Java, and AWS SDK for Java v2 versions against your project. The examples focus on small-to-medium files; high-volume or large-file workloads may benefit from presigned URLs and multipart uploads.
What you will build
- Upload a file through a Spring Boot multipart endpoint.
- Store the object in a private S3 bucket using a generated key.
- Download an object through an authenticated API or issue a short-lived presigned URL.
- Use workload IAM roles instead of embedding AWS access keys.
- Apply authorization, file validation, encryption, and operational controls.
1. Architecture
The companion SVG shows a client connecting over HTTPS to Spring Boot. The application validates the request, checks user permissions, and accesses a private S3 bucket using an IAM role. Optional metadata (owner, object key, original filename, size, timestamps) can be stored in a database. Logs and metrics can be sent to CloudWatch.
Core principles:
- Keep S3 Block Public Access enabled.
- Apply least-privilege IAM and bucket policies.
- Use an IAM role for EC2/ECS workloads instead of long-lived keys.
- Use HTTPS and encryption at rest; use SSE-KMS if required by policy.
- Check authorization before every download or presigned URL issuance.
- Treat presigned URLs as bearer tokens: anyone possessing a valid URL may use it until expiry.
- Never trust the client filename or the request
Content-Typeas proof of file safety.
2. Create the bucket
- Create an S3 bucket in the required AWS Region.
- Keep Block Public Access enabled.
- Prefer Object Ownership with ACLs disabled unless a reviewed integration requires ACLs.
- Confirm default encryption and any required KMS key policy.
- Enable versioning if recovery from accidental overwrite/deletion is needed.
- Add lifecycle rules for retention or archival where appropriate.
- Configure audit logging/CloudTrail data events according to organizational requirements.
Do not make a bucket public just to make downloads easy. Use backend authorization or short-lived presigned URLs.
3. Add the AWS SDK dependency
Use AWS SDK for Java v2. Manage versions consistently through your dependency management or an AWS SDK BOM; check compatibility instead of copying an arbitrary version.
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
The sample also uses Spring Web. Add Bean Validation if you use validation annotations. For presigned URLs, add the s3-presigner module at the same AWS SDK version.
4. Configure properties
app:
storage:
s3:
bucket: ${S3_BUCKET}
region: ${AWS_REGION:ap-south-1}
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 12MB
The sizes and Region are examples. Choose values based on business requirements, infrastructure limits, and data-residency requirements. Set values through the deployment environment or an approved configuration service; do not commit production secrets.
5. Configure the S3 client
The AWS SDK default credentials provider chain can use supported local profiles and AWS workload roles.
package com.example.files.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
@Configuration
public class S3Config {
@Bean
S3Client s3Client(@Value("${app.storage.s3.region}") String region) {
return S3Client.builder().region(Region.of(region)).build();
}
}
Attach a least-privilege role to EC2 or the ECS task. Avoid static production access keys in source code, application properties, container images, or logs.
6. Generate an object key
Do not use the raw client filename as the S3 key. Filenames can contain path-like strings, sensitive information, or duplicate names. Generate a key and store the original display name separately if needed.
package com.example.files.service;
import java.util.UUID;
import org.springframework.stereotype.Component;
@Component
public class ObjectKeyFactory {
public String newKey(String originalFilename) {
String extension = "";
if (originalFilename != null) {
int dot = originalFilename.lastIndexOf('.');
if (dot >= 0 && dot < originalFilename.length() - 1) {
String candidate = originalFilename.substring(dot + 1);
if (candidate.matches("[A-Za-z0-9]{1,10}")) {
extension = "." + candidate.toLowerCase();
}
}
}
return "uploads/" + UUID.randomUUID() + extension;
}
}
An extension does not prove a file's real content type. Use a business-specific allowlist and inspect file signatures where appropriate.
7. Upload service
This service rejects empty files and stores the object under a generated key. validatedContentType must come from your validation logic, not be blindly trusted from the request.
package com.example.files.service;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
@Service
public class S3FileService {
private final S3Client s3;
private final String bucket;
private final ObjectKeyFactory keys;
public S3FileService(S3Client s3,
@Value("${app.storage.s3.bucket}") String bucket,
ObjectKeyFactory keys) {
this.s3 = s3;
this.bucket = bucket;
this.keys = keys;
}
public UploadedFile upload(MultipartFile file, String validatedContentType)
throws IOException {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("File must not be empty");
}
String key = keys.newKey(file.getOriginalFilename());
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.contentType(validatedContentType)
.build();
try (InputStream input = file.getInputStream()) {
s3.putObject(request, RequestBody.fromInputStream(input, file.getSize()));
}
return new UploadedFile(key, file.getOriginalFilename(),
file.getSize(), validatedContentType);
}
public record UploadedFile(
String key, String originalFilename, long size, String contentType) {}
}
For large objects, review memory and buffering behavior, concurrency, timeouts, and multipart-upload support. Consider malware scanning where appropriate. In a production application, map validation failures and AWS SDK exceptions to controlled HTTP responses rather than exposing internal details.
8. REST upload endpoint
package com.example.files.controller;
import com.example.files.service.S3FileService;
import java.io.IOException;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/files")
public class FileUploadController {
private final S3FileService service;
public FileUploadController(S3FileService service) {
this.service = service;
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public S3FileService.UploadedFile upload(
@RequestPart("file") MultipartFile file) throws IOException {
// Replace with an allowlist and content inspection.
String validatedType = validateAndDetectContentType(file);
return service.upload(file, validatedType);
}
private String validateAndDetectContentType(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("File must not be empty");
}
// Demo placeholder: implement file-signature/content validation
// for your allowed file types before returning a content type.
return "application/octet-stream";
}
}
The detection method is intentionally a placeholder: implement validation appropriate to your accepted formats. Add authentication, authorization, rate limits, per-user quotas, and request-size controls. Do not accept arbitrary uploads without considering executable or malicious content.
9. Download an object through the API
Before downloading, verify that the authenticated caller is authorized to access the requested object. The SDK returns a stream; avoid reading large objects into a byte array.
import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
public ResponseInputStream<GetObjectResponse> openDownload(String key) {
// Perform an ownership/permission check before this method is called.
return s3.getObject(GetObjectRequest.builder()
.bucket(bucket)
.key(key)
.build());
}
The REST controller should stream the response and close the stream when complete. Use a safe Content-Disposition filename, a validated content type, and consider X-Content-Type-Options: nosniff. Do not let users submit arbitrary object keys without an authorization check. For large downloads, a presigned URL may reduce application-server bandwidth.
10. Presigned download URLs
A presigned URL grants temporary access to a specific object without making the bucket public. The signer must have permission for the operation. Configure S3Presigner as a Spring bean using the same Region and workload credentials.
import java.time.Duration;
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;
public String createDownloadUrl(S3Presigner presigner, String bucket, String key) {
var getObject = GetObjectRequest.builder()
.bucket(bucket).key(key).build();
var request = GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(5))
.getObjectRequest(getObject)
.build();
return presigner.presignGetObject(request).url().toString();
}
Five minutes is an example, not a universal value. Use the shortest lifetime that supports the workflow. Authorize the user before issuing the URL, do not log the full URL, and remember that the URL is usable by anyone who obtains it until it expires (subject to its signed permissions and credential lifetime).
11. Example least-privilege IAM policy
Replace YOUR_BUCKET and review this example against the exact operations your application needs. If the app never lists objects, omit s3:ListBucket. Grant deletion only if required.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListUploadsPrefixOnlyIfNeeded",
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::YOUR_BUCKET",
"Condition": {
"StringLike": {
"s3:prefix": ["uploads/*"]
}
}
},
{
"Sid": "ReadWriteUploadObjects",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::YOUR_BUCKET/uploads/*"
}
]
}
If using SSE-KMS with a customer-managed key, review the required KMS permissions and key policy. Also account for organization policies, VPC endpoint policies, and bucket policies. Keep roles and buckets separated by environment where practical.
12. Production security checklist
- S3 Block Public Access is enabled.
- Workload IAM role is used; no hard-coded production keys.
- Permissions are restricted to the required bucket and prefix.
- HTTPS and encryption at rest are enabled.
- Download authorization is checked for every object.
- Upload size, request rate, allowed types, and user quotas are enforced.
- Generated object keys are used instead of raw filenames.
- File signatures are checked where appropriate; malware scanning is considered.
- Presigned URLs are short-lived and not logged or exposed in analytics.
- Retention, versioning, lifecycle, audit, and deletion rules are defined.
- Errors do not expose credentials, internal paths, or unnecessary bucket details.
- Monitoring covers failed requests, unusual volume, and access-denied events.
13. Troubleshooting
| Symptom | What to check |
|---|---|
AccessDenied | Workload role, bucket policy, KMS key policy, prefix, organization/SCP controls |
| Region or redirect error | Bucket Region and SDK client Region |
| Upload rejected | Spring multipart limits, proxy limits, request size, content validation |
| Download returns 404 | Object key/existence and whether unauthorized access is intentionally masked |
| Presigned URL fails | Expiry, signing Region, signer permissions, URL changes, temporary credential expiry |
| Slow or memory-heavy uploads | Multipart approach, buffering, concurrency, timeouts, request limits |
| Works locally but not in AWS | Instance/task role, network path, DNS, endpoint policy, role attachment |
14. Test plan
- Upload a valid small file and verify its contents and metadata.
- Reject empty, oversized, disallowed, and malformed files.
- Verify one user cannot download another user's object.
- Confirm anonymous callers cannot upload or download through the API.
- Verify presigned URLs expire and cannot access other keys.
- Test permissions with the actual workload role.
- Exercise S3 timeout/error handling in a non-production environment.
- Verify retention and deletion behavior using a test bucket.
Conclusion
Amazon S3 provides durable object storage for files associated with a Spring Boot application, while the application remains responsible for authentication, authorization, validation, and business metadata. Keep the bucket private, use workload IAM roles, generate object keys, enforce upload controls, and choose API streaming or presigned URLs based on file size and traffic.
Official documentation
- Amazon S3 User Guide: https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html
- AWS SDK for Java 2.x: https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/home.html
- S3 presigned URLs: https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html
- IAM policies for S3: https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-policy-language-overview.html
- S3 Block Public Access: https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html