[Backend] Spring boot + React + AWS smol project (1)
Being a newbie of Java, I created a simple project for practicing. The following image is the structure of this project.
This small project provides two functions: user login and mp3 file upload. These functions are designed in Spring boot framework. As for data storage, mp3 files are stored in ASW S3 and metadata is stored in MongoDB cloud . Also, Frontend UI is completed in Tailwind and React.
Backend — Spring boot & AWS S3 initialization
- Using Spring Initialize Web to create spring boot project.
2. If you are using Ecilipse, you can follow this instruction to import created project.
3. Create a AWS account and copy the AWS SDK to java. For example, aws-java-sdk-s3 and aws-java-sdk-bom.
// pom.xml
// add aws-java-sdk-s3 to dependencies part
...
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-bom</artifactId>
<version>1.12.695</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
4. Configuring public access settings to avoid 403 (Access Denied)
response in S3 bucket
- Choose the name of the bucket that you have configured as a static website.
- Choose Permissions.
- Under Block public access (bucket settings), choose Edit.
- Clear Block all public access, and choose Save changes.
5. Edit access control list (ACL) for each items
Backend — Spring boot development
In this paragraph, only highlight some important parts. Detail code reference, please check my Github repository. Here is my backend folder schema and the main purpose of each folder.
Extends MongoDB repository
Mongodb repository provides methods to easily query data from different key. For example, user login requires email and password, so using findBy… to query the certain user.
# src/main/java/v02202/MsMusic/repository/UsersRepository.java
public interface UsersRepository extends MongoRepository <Users, String> {
Boolean existsByEmailAndPassword(String email, String password);
Boolean existsByEmail(String email);
Users findByEmailAndPassword(String email, String password);
}
Then, use UserRepository in login API.
# src/main/java/v02202/MsMusic/controller/LoginController.java
@PostMapping("/api/login")
public ResponseEntity<Users> postLoginData (@RequestBody Users users){
if(usersRepository.existsByEmailAndPassword(users.getEmail(), users.getPassword())){
Users userData = usersRepository.findByEmailAndPassword(users.getEmail(), users.getPassword());
return ResponseEntity.ok(userData);
} else {
return ResponseEntity.notFound().build();
}
}
Media upload service
In StorageService class, I defined some necessary actions on ASW S3 like upload and delete. First, build a AmazonS3ClientBuilder to connect S3 in StorageService class. AWS access key and secret key are needed here, we can read the sensitive data from .env or application.properites files through this commend @Value("${aws.iam.access.key}")
.
@Autowired
public StorageService(@Value("${aws.iam.access.key}") String accessKey, @Value("${aws.iam.secret.key}") String secretKey) {
this.accessKey = accessKey;
this.secretKey = secretKey;
AWSCredentials awsCredentials =
new BasicAWSCredentials(this.accessKey, this.secretKey);
space = AmazonS3ClientBuilder
.standard()
.withRegion(Regions.AP_NORTHEAST_1)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
Next, use the space instance created above, do putObject and deleteObject actions. In deleteObject, define your own bucket name from S3 project . ObjectKey is the filename of object wanted to delete.
public void deleteSong(String objectKey) throws IOException{
String bucketName = "msmusicbucket";
try {
space.deleteObject(bucketName, objectKey);
// amazonS3.deleteObject(new DeleteObjectRequest(awsS3AudioBucket, fileName));
} catch (AmazonServiceException ex) {
System.out.println("error [" + ex.getMessage() + "] occurred while removing file ");
}
}
public void uploadSong(MultipartFile file) throws IOException{
ObjectMetadata objectMetaData = new ObjectMetadata();
objectMetaData.setContentType(file.getContentType());
space.putObject(new PutObjectRequest(
"msmusicbucket",
file.getOriginalFilename(),
file.getInputStream(),
objectMetaData).withCannedAcl(CannedAccessControlList.PublicRead ));
}
Security configuration
During the development, I faced a CROS error because frontend and backend are set in different port. When frontend (localhost:3000) called the API from backend (localhost:8080), browser would block the request. Therefore, we need to add AllowedOrigins to backend configurations. Besides, in order to pass the preflight request which is send in OPTION method, we also need to allow OPTION method to AllowedMethods.
First, create a config folder to root folder, and add a SecurityConfig class.
The above image is the role of SecurityFilterChain. From the official document, SecurityFilterChain are registered with FilterChainProxy. FilterChainProxy decides which SecurityFilterChain should be used, and it depends on the request url pattern. For example, if we define the SecurityFilterChain0
pattern of /api/**
then, the request of /api/message
would be checked in SecurityFilterChain0
. Authentication is checked in SecurityFilterChain as well. If any request are required authenticated, authorizeRequests
can be added to securityFilterChain.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.cors(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.csrf(AbstractHttpConfigurer::disable)
.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setExposedHeaders(Arrays.asList("Access-Control-Allow-Origin"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
System.out.println("Configuring security .....");
return source;
}
}
CorsConfigurationSource is the important part for CORS problem. From the above code, setAllowedOrigins allows all origins and setAllowedMethods allows all methods.
Some bugs
During development, I faced some error messages that took me a while to solve them.
- Using debug mode to test my project, but encountered
No qualifying bean of type service
error.
Solution: The file location is incorrect; the models, service, and repository folders should be placed under root folder (in my case, src/main/java/v02202/MsMusic). This way, beem can find them. - Upload file to S3, but encountered
com.amazonaws.services.s3.model.AmazonS3Exception: The bucket is in this region: us-east-1.
Solution: At first, I thought it was resulted from the mistake of region setting in S3. However, I checked all the setting and found the reason is bucket name. I made a typo in the bucket name in uploadSong class. - Spring boot keeps connecting localhost:27017 instead of mongodb uri set in the application.properties.
Solution: Check the application.properties in target folder. (I removed the target folder and placed it back, then reset the application.properties, then things go fine.)
Part 1 is ended here, you can check my second article for dockerizing this project. Also, this is my firs t time share the deployment note in English. Feel free to give me any suggestion. Thanks for your reading, you can check the detail from my repo.