From 021675f4076dc1d205752de3c937c97a4e2bf0b4 Mon Sep 17 00:00:00 2001 From: geniuus Date: Thu, 4 Jul 2024 22:44:58 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Build=20[#20]=20Amazon=20S3=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jaksim/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jaksim/build.gradle b/jaksim/build.gradle index 21357c0..e63e44b 100644 --- a/jaksim/build.gradle +++ b/jaksim/build.gradle @@ -48,6 +48,10 @@ dependencies { // implementation 'org.springframework.boot:spring-boot-starter-websocket' implementation 'com.corundumstudio.socketio:netty-socketio:2.0.9' + // Multipart File - Amazon S3 + implementation("software.amazon.awssdk:bom:2.21.0") + implementation("software.amazon.awssdk:s3:2.21.0") + // test testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' From 6e1a92d962326caafe1382e84b799302c2f83e19 Mon Sep 17 00:00:00 2001 From: geniuus Date: Thu, 4 Jul 2024 22:45:22 +0900 Subject: [PATCH 2/4] =?UTF-8?q?Feat=20[#20]=20White=20List=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jaksim/src/main/java/org/sopt/jaksim/auth/SecurityConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jaksim/src/main/java/org/sopt/jaksim/auth/SecurityConfig.java b/jaksim/src/main/java/org/sopt/jaksim/auth/SecurityConfig.java index 9cc772a..9a056e8 100644 --- a/jaksim/src/main/java/org/sopt/jaksim/auth/SecurityConfig.java +++ b/jaksim/src/main/java/org/sopt/jaksim/auth/SecurityConfig.java @@ -29,7 +29,8 @@ public class SecurityConfig { private final CustomAccessDeniedHandler customAccessDeniedHandler; private static final String[] AUTH_WHITE_LIST = {ACTIVATE_PROFILE_URL, - "/login/**", "/api/v1/auth/**", + "/api/v1/user/signin/**", "/api/v1/auth/**", + "/api/v1/user/reissue/**", "/socket.io/**", "/swagger-ui/**", "/swagger-resources/**"}; @Bean From fcde71d237970b6e8fc7a49896463561601e5327 Mon Sep 17 00:00:00 2001 From: geniuus Date: Thu, 4 Jul 2024 22:45:35 +0900 Subject: [PATCH 3/4] =?UTF-8?q?Feat=20[#20]=20S3=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/jaksim/global/common/S3Service.java | 81 +++++++++++++++++++ .../sopt/jaksim/global/config/AwsConfig.java | 48 +++++++++++ 2 files changed, 129 insertions(+) create mode 100644 jaksim/src/main/java/org/sopt/jaksim/global/common/S3Service.java create mode 100644 jaksim/src/main/java/org/sopt/jaksim/global/config/AwsConfig.java diff --git a/jaksim/src/main/java/org/sopt/jaksim/global/common/S3Service.java b/jaksim/src/main/java/org/sopt/jaksim/global/common/S3Service.java new file mode 100644 index 0000000..a70002d --- /dev/null +++ b/jaksim/src/main/java/org/sopt/jaksim/global/common/S3Service.java @@ -0,0 +1,81 @@ +package org.sopt.jaksim.global.common; + +import org.sopt.jaksim.global.config.AwsConfig; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +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.Arrays; +import java.util.List; +import java.util.UUID; + +@Component +public class S3Service { + + private final String bucketName; + private final AwsConfig awsConfig; + private static final List IMAGE_EXTENSIONS = Arrays.asList("image/jpeg", "image/png", "image/jpg", "image/webp"); + + + public S3Service(@Value("${aws-property.s3-bucket-name}") final String bucketName, AwsConfig awsConfig) { + this.bucketName = bucketName; + this.awsConfig = awsConfig; + } + + + public String uploadImage(String directoryPath, MultipartFile image) throws IOException { + final String key = directoryPath + generateImageFileName(); + final S3Client s3Client = awsConfig.getS3Client(); + + validateExtension(image); + validateFileSize(image); + + PutObjectRequest request = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .contentType(image.getContentType()) + .contentDisposition("inline") + .build(); + + RequestBody requestBody = RequestBody.fromBytes(image.getBytes()); + s3Client.putObject(request, requestBody); + return key; + } + + public void deleteImage(String key) throws IOException { + final S3Client s3Client = awsConfig.getS3Client(); + + s3Client.deleteObject((DeleteObjectRequest.Builder builder) -> + builder.bucket(bucketName) + .key(key) + .build() + ); + } + + + private String generateImageFileName() { + return UUID.randomUUID() + ".jpg"; + } + + + private void validateExtension(MultipartFile image) { + String contentType = image.getContentType(); + if (!IMAGE_EXTENSIONS.contains(contentType)) { + throw new RuntimeException("이미지 확장자는 jpg, png, webp만 가능합니다."); + } + } + + private static final Long MAX_FILE_SIZE = 5 * 1024 * 1024L; + + private void validateFileSize(MultipartFile image) { + if (image.getSize() > MAX_FILE_SIZE) { + throw new RuntimeException("이미지 사이즈는 5MB를 넘을 수 없습니다."); + } + } + +} diff --git a/jaksim/src/main/java/org/sopt/jaksim/global/config/AwsConfig.java b/jaksim/src/main/java/org/sopt/jaksim/global/config/AwsConfig.java new file mode 100644 index 0000000..aa57ff8 --- /dev/null +++ b/jaksim/src/main/java/org/sopt/jaksim/global/config/AwsConfig.java @@ -0,0 +1,48 @@ +package org.sopt.jaksim.global.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.SystemPropertyCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +@Configuration +public class AwsConfig { + + private static final String AWS_ACCESS_KEY_ID = "aws.accessKeyId"; + private static final String AWS_SECRET_ACCESS_KEY = "aws.secretAccessKey"; + + private final String accessKey; + private final String secretKey; + private final String regionString; + + public AwsConfig(@Value("${aws-property.access-key}") final String accessKey, + @Value("${aws-property.secret-key}") final String secretKey, + @Value("${aws-property.aws-region}") final String regionString) { + this.accessKey = accessKey; + this.secretKey = secretKey; + this.regionString = regionString; + } + + + @Bean + public SystemPropertyCredentialsProvider systemPropertyCredentialsProvider() { + System.setProperty(AWS_ACCESS_KEY_ID, accessKey); + System.setProperty(AWS_SECRET_ACCESS_KEY, secretKey); + return SystemPropertyCredentialsProvider.create(); + } + + @Bean + public Region getRegion() { + return Region.of(regionString); + } + + @Bean + public S3Client getS3Client() { + return S3Client.builder() + .region(getRegion()) + .credentialsProvider(systemPropertyCredentialsProvider()) + .build(); + } +} From 7b30e4f14aad094f0968f2c473453fc09ddafd11 Mon Sep 17 00:00:00 2001 From: geniuus Date: Thu, 4 Jul 2024 22:46:00 +0900 Subject: [PATCH 4/4] =?UTF-8?q?Fix=20[#20]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,?= =?UTF-8?q?=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20API=20Path=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit user -> users --- .../main/java/org/sopt/jaksim/user/api/UserApiController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jaksim/src/main/java/org/sopt/jaksim/user/api/UserApiController.java b/jaksim/src/main/java/org/sopt/jaksim/user/api/UserApiController.java index 5589fb4..01ecc7b 100644 --- a/jaksim/src/main/java/org/sopt/jaksim/user/api/UserApiController.java +++ b/jaksim/src/main/java/org/sopt/jaksim/user/api/UserApiController.java @@ -32,13 +32,13 @@ public ResponseEntity> signup(@RequestBody final UserSignUpReque } @Override - @PostMapping("/user/signin") + @PostMapping("/users/signin") public ResponseEntity> signin(@RequestHeader(AUTHORIZATION) final String accessToken) { final UserSignInResponse response = userFacade.signin(); return ApiResponseUtil.success(SuccessMessage.USER_SIGN_IN_SUCCESS, response); } - @PostMapping("/reissue") + @PostMapping("/users/reissue") public ResponseEntity> reissue(@RequestHeader(AUTHORIZATION) final String refreshToken, @RequestBody final UserReissueRequest userReissueRequest) { UserSignInResponse response = userFacade.reissue(refreshToken, userReissueRequest);