Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#Spring Boot를 Docker 이미지로 포장하는 설명서

# syntax=docker/dockerfile:1
FROM eclipse-temurin:17-jdk-jammy AS build
WORKDIR /app
Expand Down
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation platform('software.amazon.awssdk:bom:2.55.5')
implementation 'software.amazon.awssdk:s3'
// Jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
Expand Down
8 changes: 8 additions & 0 deletions compose.deploy.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# EC2에서 Docker 이미지를 어떻게 실행할지 정하는 파일

# ECR의 백엔드 이미지 선택
# prod 환경 활성화
# EC2의 비밀 환경변수 전달
# EC2의 80번 포트를 Spring Boot 8080번 포트에 연결
# 서버가 꺼지면 컨테이너 재시작

services:
backend:
image: ${IMAGE_URI:?IMAGE_URI must be set}
Expand Down
8 changes: 6 additions & 2 deletions deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ Complete these one-time steps before merging the CI/CD pull request:
Use the RDS endpoint from the RDS console and the password already chosen
when creating the DB. Generate a separate JWT secret on EC2 with
`openssl rand -hex 32`; do not paste it into GitHub or a chat. Run
`sudo chmod 600 /opt/mulmi/backend.env` afterward. No AWS access keys are
needed because EC2 has `MulmiEc2Role`.
`sudo chmod 600 /opt/mulmi/backend.env` afterward. Set
`CORS_ALLOWED_ORIGINS` to the frontend origin. Multiple origins must be
comma-separated. No AWS access keys are needed because EC2 has
`MulmiEc2Role`. Keep `S3_BUCKET=mulmi-dev-files-563586109665`; the role is
limited to that bucket and the application uses the EC2 instance profile
automatically.

The development Compose file temporarily uses Hibernate `ddl-auto=update`
because this database is empty and the project has no schema migrations yet.
Expand Down
2 changes: 2 additions & 0 deletions deploy/backend.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ DB_URL=jdbc:mysql://YOUR_RDS_ENDPOINT:3306/mulmi?sslMode=REQUIRED&serverTimezone
DB_USERNAME=admin
DB_PASSWORD=YOUR_RDS_PASSWORD
JWT_TOKEN_SECRETKEY=YOUR_RANDOM_SECRET_OF_AT_LEAST_32_CHARACTERS
CORS_ALLOWED_ORIGINS=https://YOUR_FRONTEND_DOMAIN
S3_BUCKET=mulmi-dev-files-563586109665
20 changes: 0 additions & 20 deletions docker-compose.local.yml

This file was deleted.

34 changes: 0 additions & 34 deletions docker-compose.yml

This file was deleted.

24 changes: 24 additions & 0 deletions src/main/java/com/mulmi/backend/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.mulmi.backend.global.jwt.JwtAuthenticationFilter;
import com.mulmi.backend.global.jwt.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand All @@ -12,6 +13,11 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@RequiredArgsConstructor
Expand All @@ -20,9 +26,13 @@ public class SecurityConfig {

private final JwtUtil jwtUtil;

@Value("${app.cors.allowed-origins}")
private List<String> allowedOrigins;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.formLogin(formLogin -> formLogin.disable())
.httpBasic(httpBasic -> httpBasic.disable())
Expand Down Expand Up @@ -50,6 +60,20 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(allowedOrigins);
configuration.setAllowedMethods(List.of("GET", "POST", "PATCH", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.mulmi.backend.global.storage;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
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
@EnableConfigurationProperties(S3StorageProperties.class)
public class S3StorageConfig {

@Bean
public S3Client s3Client(S3StorageProperties properties) {
return S3Client.builder()
.region(Region.of(properties.region()))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.mulmi.backend.global.storage;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "app.storage.s3")
public record S3StorageProperties(
String region,
String bucket
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.mulmi.backend.global.storage;

import lombok.RequiredArgsConstructor;
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.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.io.IOException;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Service
@RequiredArgsConstructor
public class S3StorageService {

private static final Pattern FILE_EXTENSION_PATTERN =
Pattern.compile("\\.([a-zA-Z0-9]{1,10})$");

private final S3Client s3Client;
private final S3StorageProperties properties;

public String upload(MultipartFile file, String directory) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("업로드할 파일이 필요합니다.");
}

String objectKey = createObjectKey(directory, file.getOriginalFilename());
PutObjectRequest request = PutObjectRequest.builder()
.bucket(properties.bucket())
.key(objectKey)
.contentType(Optional.ofNullable(file.getContentType())
.orElse("application/octet-stream"))
.build();

try {
s3Client.putObject(
request,
RequestBody.fromInputStream(file.getInputStream(), file.getSize())
);
} catch (IOException exception) {
throw new IllegalStateException("파일을 읽을 수 없습니다.", exception);
}

return objectKey;
}

public void delete(String objectKey) {
DeleteObjectRequest request = DeleteObjectRequest.builder()
.bucket(properties.bucket())
.key(objectKey)
.build();

s3Client.deleteObject(request);
}

private String createObjectKey(String directory, String originalFilename) {
String extension = extractExtension(originalFilename);
String filename = UUID.randomUUID() + extension;

if (directory == null || directory.isBlank()) {
return filename;
}

String normalizedDirectory = directory.replace('\\', '/').replaceAll("^/+|/+$", "");
if (normalizedDirectory.contains("..")) {
throw new IllegalArgumentException("올바르지 않은 파일 저장 경로입니다.");
}
return normalizedDirectory + "/" + filename;
}

private String extractExtension(String originalFilename) {
if (originalFilename == null) {
return "";
}

Matcher matcher = FILE_EXTENSION_PATTERN.matcher(originalFilename);
if (!matcher.find()) {
return "";
}

return "." + matcher.group(1).toLowerCase(Locale.ROOT);
}
}
9 changes: 9 additions & 0 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# 로컬에서의 스프링부트 설정
spring:
config:
activate:
Expand All @@ -18,3 +19,11 @@ spring:
format_sql: true
jdbc:
time_zone: Asia/Seoul

jwt:
token:
secret-key: ${JWT_TOKEN_SECRETKEY:ZGh3YWlkc2F2ZXdhZXZ3b2EgMTM5ZXUgMDMxdWMyIHEyMiBAIDAgKTJFVio=}

app:
cors:
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:5173}
5 changes: 5 additions & 0 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#EC2의 Docker에서 실행할 때의 스프링부트 설정
spring:
config:
activate:
Expand Down Expand Up @@ -34,3 +35,7 @@ logging:
jwt:
token:
secret-key: ${JWT_TOKEN_SECRETKEY}

app:
cors:
allowed-origins: ${CORS_ALLOWED_ORIGINS}
8 changes: 7 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# 스프링부트 공통 설정!
spring:
application:
name: mulmi-backend
Expand All @@ -16,6 +17,11 @@ server:

jwt:
token:
secretKey: ZGh3YWlkc2F2ZXdhZXZ3b2EgMTM5ZXUgMDMxdWMyIHEyMiBAIDAgKTJFVio=
expiration:
access: 14400000

app:
storage:
s3:
region: ${AWS_REGION:ap-northeast-2}
bucket: ${S3_BUCKET:mulmi-dev-files-563586109665}
Loading