AWS S3 Basics: Storing Images with Spring Boot
Amazon S3 (Simple Storage Service) is object storage built to store and retrieve any amount of data. It's perfect for storing images, documents, videos, and other files. This guide shows you how to integrate S3 with Spring Boot to store and serve images.
What is AWS S3?
S3 is AWS's object storage service. Key concepts:
- Buckets: Containers for objects (like folders, but at the root level)
- Objects: Files stored in buckets
- Keys: Unique identifiers for objects (like file paths)
- Regions: Geographic location where data is stored
Benefits:
- Scalable: Store unlimited data
- Durable: 99.999999999% (11 9's) durability
- Fast: Low latency access
- Secure: Encryption and access controls
- Cost-effective: Pay only for what you use
Step 1: Create an S3 Bucket
Using AWS Console
- Log in to AWS Console
- Navigate to S3 service
- Click "Create bucket"
Bucket Configuration:
General Configuration:
- Bucket name:
my-app-images(must be globally unique) - AWS Region: Choose closest region (e.g.,
us-east-1) - Object Ownership: ACLs disabled (recommended)
Block Public Access settings:
- Uncheck "Block all public access" if you want public images
- For private images, keep all boxes checked
Bucket Versioning:
- Disable (for now, enable later if needed)
Default Encryption:
- Enable
- Encryption type: Amazon S3 managed keys (SSE-S3)
Advanced settings:
- Keep defaults
- Click "Create bucket"
Important: Bucket names must be globally unique across all AWS accounts!
Step 2: Configure Bucket Permissions
For Private Images (Recommended)
Keep bucket private and access via application:
- Click on your bucket
- Go to "Permissions" tab
- Ensure "Block public access" is enabled
- Your application will access images using IAM credentials
For Public Images
If you want images publicly accessible:
- Click on your bucket
- Go to "Permissions" tab
- Unblock public access (uncheck all boxes)
- Add bucket policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-app-images/*"
}
]
}
- Click "Save changes"
Security Note: Public buckets expose all objects. Use CORS and bucket policies carefully.
Step 3: Set Up IAM Permissions
Option 1: IAM Role (Recommended for EC2)
Create IAM role with S3 permissions:
- Go to IAM Console
- Click "Roles" → "Create role"
- Select "AWS service" → "EC2"
- Attach policy: "AmazonS3FullAccess" (or create custom policy)
- Name role: "ec2-s3-access-role"
- Attach role to EC2 instance
Option 2: IAM User (For Local Development)
- Create IAM user
- Attach policy: "AmazonS3FullAccess"
- Create access keys
- Configure AWS credentials locally
Custom Policy Example (More restrictive):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::my-app-images/*"
},
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-app-images"
}
]
}
Step 4: Add Dependencies to Spring Boot
Maven (pom.xml)
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- AWS SDK for S3 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.20.0</version>
</dependency>
</dependencies>
Gradle (build.gradle)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'software.amazon.awssdk:s3:2.20.0'
}
Step 5: Configure AWS Properties
application.properties
# AWS Configuration
aws.region=us-east-1
aws.s3.bucket-name=my-app-images
# File Upload Settings
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
application.yml (Alternative)
aws:
region: us-east-1
s3:
bucket-name: my-app-images
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
Step 6: Create S3 Service
S3Configuration
package com.example.s3.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
@Configuration
public class S3Config {
@Value("${aws.region:us-east-1}")
private String region;
@Bean
public S3Client s3Client() {
return S3Client.builder()
.region(Region.of(region))
.credentialsProvider(DefaultCredentialsProvider.create())
.build();
}
}
S3Service
package com.example.s3.service;
import org.springframework.beans.factory.annotation.Autowired;
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.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Service
public class S3Service {
private final S3Client s3Client;
@Value("${aws.s3.bucket-name}")
private String bucketName;
@Autowired
public S3Service(S3Client s3Client) {
this.s3Client = s3Client;
}
/**
* Upload a file to S3
* @param file The file to upload
* @param folder Optional folder prefix (e.g., "images", "documents")
* @return The S3 URL of the uploaded file
*/
public String uploadFile(MultipartFile file, String folder) throws IOException {
// Generate unique file name
String fileName = generateFileName(file.getOriginalFilename());
String key = folder != null ? folder + "/" + fileName : fileName;
// Upload file
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.contentType(file.getContentType())
.build();
s3Client.putObject(putObjectRequest, RequestBody.fromBytes(file.getBytes()));
// Return URL
return getFileUrl(key);
}
/**
* Upload a file to S3 (root level)
*/
public String uploadFile(MultipartFile file) throws IOException {
return uploadFile(file, null);
}
/**
* Download a file from S3
* @param key The S3 key (path) of the file
* @return The file content as byte array
*/
public byte[] downloadFile(String key) {
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
return s3Client.getObjectAsBytes(getObjectRequest).asByteArray();
}
/**
* Delete a file from S3
* @param key The S3 key (path) of the file to delete
*/
public void deleteFile(String key) {
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
s3Client.deleteObject(deleteObjectRequest);
}
/**
* List all files in a folder
* @param folder The folder prefix
* @return List of file keys
*/
public List<String> listFiles(String folder) {
ListObjectsV2Request listRequest = ListObjectsV2Request.builder()
.bucket(bucketName)
.prefix(folder != null ? folder + "/" : "")
.build();
ListObjectsV2Response response = s3Client.listObjectsV2(listRequest);
List<String> keys = new ArrayList<>();
for (S3Object s3Object : response.contents()) {
keys.add(s3Object.key());
}
return keys;
}
/**
* Check if file exists
* @param key The S3 key
* @return true if file exists
*/
public boolean fileExists(String key) {
try {
HeadObjectRequest headRequest = HeadObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
s3Client.headObject(headRequest);
return true;
} catch (NoSuchKeyException e) {
return false;
}
}
/**
* Generate unique file name
*/
private String generateFileName(String originalFileName) {
String extension = "";
if (originalFileName != null && originalFileName.contains(".")) {
extension = originalFileName.substring(originalFileName.lastIndexOf("."));
}
return UUID.randomUUID().toString() + extension;
}
/**
* Get public URL of file
*/
private String getFileUrl(String key) {
return String.format("https://%s.s3.%s.amazonaws.com/%s",
bucketName,
s3Client.serviceClientConfiguration().region().id(),
key);
}
}
Step 7: Create Image Controller
ImageController
package com.example.s3.controller;
import com.example.s3.service.S3Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/images")
public class ImageController {
private final S3Service s3Service;
@Autowired
public ImageController(S3Service s3Service) {
this.s3Service = s3Service;
}
/**
* Upload an image
*/
@PostMapping("/upload")
public ResponseEntity<Map<String, String>> uploadImage(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "folder", required = false) String folder) {
try {
// Validate file
if (file.isEmpty()) {
return ResponseEntity.badRequest()
.body(createErrorResponse("File is empty"));
}
// Validate image type
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
return ResponseEntity.badRequest()
.body(createErrorResponse("File must be an image"));
}
// Upload to S3
String url = s3Service.uploadFile(file, folder != null ? folder : "images");
Map<String, String> response = new HashMap<>();
response.put("message", "Image uploaded successfully");
response.put("url", url);
return ResponseEntity.ok(response);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(createErrorResponse("Error uploading file: " + e.getMessage()));
}
}
/**
* Download an image
*/
@GetMapping("/download/{key}")
public ResponseEntity<byte[]> downloadImage(@PathVariable String key) {
try {
byte[] imageData = s3Service.downloadFile("images/" + key);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_JPEG); // Adjust based on file type
headers.setContentLength(imageData.length);
headers.setContentDispositionFormData("attachment", key);
return new ResponseEntity<>(imageData, headers, HttpStatus.OK);
} catch (Exception e) {
return ResponseEntity.notFound().build();
}
}
/**
* Delete an image
*/
@DeleteMapping("/{key}")
public ResponseEntity<Map<String, String>> deleteImage(@PathVariable String key) {
try {
s3Service.deleteFile("images/" + key);
Map<String, String> response = new HashMap<>();
response.put("message", "Image deleted successfully");
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(createErrorResponse("Error deleting file: " + e.getMessage()));
}
}
/**
* List all images
*/
@GetMapping("/list")
public ResponseEntity<List<String>> listImages(
@RequestParam(value = "folder", required = false) String folder) {
try {
List<String> files = s3Service.listFiles(folder != null ? folder : "images");
return ResponseEntity.ok(files);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
private Map<String, String> createErrorResponse(String message) {
Map<String, String> response = new HashMap<>();
response.put("error", message);
return response;
}
}
Step 8: Image Upload HTML Form (Optional)
Create a simple HTML form for testing:
<!DOCTYPE html>
<html>
<head>
<title>Image Upload</title>
</head>
<body>
<h1>Upload Image to S3</h1>
<form
action="http://localhost:8080/api/images/upload"
method="post"
enctype="multipart/form-data"
>
<input type="file" name="file" accept="image/*" required />
<br /><br />
<button type="submit">Upload Image</button>
</form>
</body>
</html>
Advanced Features
Pre-signed URLs
Generate temporary URLs for private images:
public String generatePresignedUrl(String key, int expirationMinutes) {
S3Presigner presigner = S3Presigner.create();
PresignedGetObjectPresignRequest presignRequest =
PresignedGetObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(expirationMinutes))
.getObjectRequest(r -> r.bucket(bucketName).key(key))
.build();
PresignedGetObjectRequest presignedRequest =
presigner.presignGetObject(presignRequest);
return presignedRequest.url().toString();
}
Use case: Generate temporary URLs that expire after 15 minutes for secure image sharing.
Image Resizing (Optional)
Use AWS Lambda or resize before upload:
// Using Java ImageIO (resize before upload)
private byte[] resizeImage(MultipartFile file, int maxWidth, int maxHeight) throws IOException {
BufferedImage originalImage = ImageIO.read(file.getInputStream());
int width = originalImage.getWidth();
int height = originalImage.getHeight();
if (width > maxWidth || height > maxHeight) {
double scale = Math.min((double) maxWidth / width, (double) maxHeight / height);
width = (int) (width * scale);
height = (int) (height * scale);
}
BufferedImage resizedImage = new BufferedImage(width, height, originalImage.getType());
Graphics2D g = resizedImage.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(originalImage, 0, 0, width, height, null);
g.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(resizedImage, "jpg", baos);
return baos.toByteArray();
}
Content Type Detection
private String detectContentType(String fileName) {
String extension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
switch (extension) {
case "jpg":
case "jpeg":
return "image/jpeg";
case "png":
return "image/png";
case "gif":
return "image/gif";
case "webp":
return "image/webp";
default:
return "application/octet-stream";
}
}
Best Practices
1. Use IAM Roles Instead of Access Keys
For EC2, Lambda, or ECS, use IAM roles. Never store access keys in code.
2. Organize Files with Folders
Use folder structure:
bucket-name/
├── images/
│ ├── profile/
│ ├── products/
│ └── thumbnails/
├── documents/
└── videos/
3. Set Content Types
Always set content-type when uploading:
PutObjectRequest.builder()
.contentType(file.getContentType())
.build();
4. Enable Versioning for Important Data
Enable versioning to recover from accidental deletions:
s3Client.putBucketVersioning(PutBucketVersioningRequest.builder()
.bucket(bucketName)
.versioningConfiguration(VersioningConfiguration.builder()
.status(BucketVersioningStatus.ENABLED)
.build())
.build());
5. Use Lifecycle Policies
Automatically move old files to cheaper storage:
- Move to Glacier after 90 days
- Delete old versions after 30 days
- Archive old files automatically
6. Enable Encryption
Always enable encryption:
PutObjectRequest.builder()
.serverSideEncryption(ServerSideEncryption.AES256)
.build();
7. Validate File Sizes
Set maximum file size limits:
if (file.getSize() > 10 * 1024 * 1024) { // 10MB
throw new IllegalArgumentException("File size exceeds 10MB");
}
8. Validate File Types
Restrict allowed file types:
List<String> allowedTypes = Arrays.asList("image/jpeg", "image/png", "image/gif");
if (!allowedTypes.contains(file.getContentType())) {
throw new IllegalArgumentException("File type not allowed");
}
Cost Considerations
S3 pricing (us-east-1):
- Storage: $0.023 per GB/month (first 50 GB free for 12 months)
- PUT requests: $0.005 per 1,000 requests
- GET requests: $0.0004 per 1,000 requests (first 20,000 free)
- Data transfer out: $0.09 per GB (first 1 GB free)
Cost Optimization Tips:
- Use appropriate storage classes (Standard, IA, Glacier)
- Enable lifecycle policies to move old files
- Use CloudFront for frequently accessed images
- Compress images before upload
- Delete unused files regularly
Troubleshooting
Issue: Access Denied
Check:
- IAM role/user has S3 permissions
- Bucket policy allows access
- Region is correct
- Bucket name is correct
Issue: CORS Errors
If accessing S3 from browser, configure CORS:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"AllowedOrigins": ["http://localhost:3000", "https://yourdomain.com"],
"ExposeHeaders": []
}
]
Issue: Slow Uploads
Solutions:
- Use multipart upload for large files
- Use Transfer Acceleration (additional cost)
- Upload from same region as bucket
S3 is perfect for storing images and files in your Spring Boot application. Start with basic upload/download, then add features like pre-signed URLs, image resizing, and lifecycle policies as needed.
Next Steps:
- Set up your S3 bucket
- Integrate S3 service into your Spring Boot app
- Implement image upload/download endpoints
- Add image validation and resizing
- Configure CloudFront for CDN (optional)
