From b2d81ef75ca7185ff271f4aff4ff89a6f3e7a18c Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 31 Jan 2024 02:38:58 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../auth/service/AuthService.java | 8 +- .../auth/service/KakaoOAuthService.java | 4 +- .../security/JwtAuthenticationFilter.java | 6 +- .../security/SecurityConfiguration.java | 2 +- .../custom/exception/ErrorCode.java | 5 + .../solidconnection/s3/AmazonS3Config.java | 31 ++++++ .../solidconnection/s3/ImageUrlDto.java | 11 ++ .../solidconnection/s3/S3Controller.java | 45 ++++++++ .../example/solidconnection/s3/S3Service.java | 105 ++++++++++++++++++ .../siteuser/service/SiteUserValidator.java | 20 ++++ .../example/solidconnection/type/ImgType.java | 14 +++ 12 files changed, 241 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/s3/AmazonS3Config.java create mode 100644 src/main/java/com/example/solidconnection/s3/ImageUrlDto.java create mode 100644 src/main/java/com/example/solidconnection/s3/S3Controller.java create mode 100644 src/main/java/com/example/solidconnection/s3/S3Service.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java create mode 100644 src/main/java/com/example/solidconnection/type/ImgType.java diff --git a/build.gradle b/build.gradle index b6e8719eb..c577ad641 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,7 @@ dependencies { implementation 'org.springframework.security:spring-security-web:6.1.2' implementation 'io.lettuce:lettuce-core:6.2.5.RELEASE' implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.470' compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index d98e80d04..4558dfd73 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -13,6 +13,7 @@ import com.example.solidconnection.repositories.InterestedRegionRepository; import com.example.solidconnection.repositories.RegionRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.siteuser.service.SiteUserValidator; import com.example.solidconnection.type.CountryCode; import com.example.solidconnection.type.RegionCode; import com.example.solidconnection.type.Role; @@ -38,6 +39,7 @@ public class AuthService { private final RedisTemplate redisTemplate; private final TokenValidator tokenValidator; private final TokenService tokenService; + private final SiteUserValidator siteUserValidator; private final SiteUserRepository siteUserRepository; private final RegionRepository regionRepository; private final InterestedRegionRepository interestedRegionRepository; @@ -78,15 +80,11 @@ public boolean signOut(String email){ } public boolean quit(String email){ - SiteUser siteUser = getValidatedUser(email); + SiteUser siteUser = siteUserValidator.validateExistByEmail(email); siteUser.setQuitedAt(LocalDate.now().plusDays(1)); return true; } - private SiteUser getValidatedUser(String email){ - return siteUserRepository.findByEmail(email).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - } - private void validateUserNotDuplicated(SignUpRequestDto signUpRequestDto){ String email = tokenService.getEmail(signUpRequestDto.getKakaoOauthToken()); if(siteUserRepository.existsByEmail(email)){ diff --git a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java index 4583802ba..f4b40c596 100644 --- a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java @@ -7,6 +7,7 @@ import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.entity.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.siteuser.service.SiteUserValidator; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; @@ -26,6 +27,7 @@ public class KakaoOAuthService { private final RestTemplate restTemplate; private final TokenService tokenService; + private final SiteUserValidator siteUserValidator; private final SiteUserRepository siteUserRepository; @Value("${kakao.client_id}") @@ -109,7 +111,7 @@ private SignInResponseDto kakaoSignIn(String email) { } public void resetQuitedAt(String email){ - SiteUser siteUser = siteUserRepository.findByEmail(email).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + SiteUser siteUser = siteUserValidator.validateExistByEmail(email); siteUser.setQuitedAt(null); } } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index 4b37e3077..954d7999f 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -43,7 +43,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse try { String token = this.resolveAccessTokenFromRequest(request); // 웹 요청에서 토큰 추출 - tokenValidator.validateAccessToken(token); // 유효한 액세스 토큰인지 검증 + tokenValidator.validateAccessToken(token); // 액세스 토큰 검증 - 비어있는지, 유효한지, 리프레시 토큰, 로그아웃 Authentication auth = this.tokenService.getAuthentication(token); // 토큰에서 인증 정보 가져옴 SecurityContextHolder.getContext().setAuthentication(auth);// 인증 정보를 보안 컨텍스트에 설정 filterChain.doFilter(request, response); // 다음 필터로 요청과 응답 전달 @@ -72,9 +72,7 @@ private HashSet getPermitAllEndpoints() { permitAllEndpoints.add("/favicon.ico"); // 이미지 업로드 - permitAllEndpoints.add("/img-upload/profile"); - permitAllEndpoints.add("/img-upload/gpa"); - permitAllEndpoints.add("/img-upload/language"); + permitAllEndpoints.add("/img/profile/pre"); // 토큰이 필요하지 않은 인증 permitAllEndpoints.add("/auth/kakao"); diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index bc7256b28..1012c4256 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -48,7 +48,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { -> authorizeRequest .requestMatchers( "/", "/index.html", "/favicon.ico", - "/img-upload/profile", "/img-upload/gpa", "/img-upload/language", + "/img/profile/pre", "/auth/kakao", "/auth/sign-up") .permitAll() .anyRequest().authenticated()) diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 10bd81800..98b076da7 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -7,6 +7,11 @@ @Getter @AllArgsConstructor public enum ErrorCode { + S3_SERVICE_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 서비스 에러 발생"), + S3_CLIENT_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 클라이언트 에러 발생"), + FILE_NOT_EXIST(HttpStatus.UNAUTHORIZED.value(), "파일이 없습니다."), + INVALID_FILE_EXTENSIONS(HttpStatus.UNAUTHORIZED.value(), "파일 형식이 유효하지 않습니다."), + NOT_IMG_FILE_EXTENSIONS(HttpStatus.UNAUTHORIZED.value(), "이미지만 업로드 할 수 있습니다."), USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), USER_ALREADY_EXISTED(HttpStatus.CONFLICT.value(), "이미 존재하는 회원입니다."), JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱 에러"), diff --git a/src/main/java/com/example/solidconnection/s3/AmazonS3Config.java b/src/main/java/com/example/solidconnection/s3/AmazonS3Config.java new file mode 100644 index 000000000..8f027505a --- /dev/null +++ b/src/main/java/com/example/solidconnection/s3/AmazonS3Config.java @@ -0,0 +1,31 @@ +package com.example.solidconnection.s3; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AmazonS3Config { + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder + .standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + } +} diff --git a/src/main/java/com/example/solidconnection/s3/ImageUrlDto.java b/src/main/java/com/example/solidconnection/s3/ImageUrlDto.java new file mode 100644 index 000000000..1690f56dd --- /dev/null +++ b/src/main/java/com/example/solidconnection/s3/ImageUrlDto.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.s3; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ImageUrlDto { + private String imageUrl; +} diff --git a/src/main/java/com/example/solidconnection/s3/S3Controller.java b/src/main/java/com/example/solidconnection/s3/S3Controller.java new file mode 100644 index 000000000..4b1dc7c28 --- /dev/null +++ b/src/main/java/com/example/solidconnection/s3/S3Controller.java @@ -0,0 +1,45 @@ +package com.example.solidconnection.s3; + +import com.example.solidconnection.custom.response.CustomResponse; +import com.example.solidconnection.custom.response.DataResponse; +import com.example.solidconnection.type.ImgType; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.security.Principal; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/img") +public class S3Controller { + private final S3Service s3Service; + + @PostMapping("/profile/pre") + public CustomResponse uploadPreProfileImage(@RequestParam("imageFile") MultipartFile imageFile) { + ImageUrlDto profileImageUrl = s3Service.uploadImgFile(imageFile, ImgType.PROFILE); + return new DataResponse<>(profileImageUrl); + } + + @PostMapping("/profile/post") + public CustomResponse uploadPostProfileImage(@RequestParam("imageFile") MultipartFile imageFile, Principal principal) { + ImageUrlDto profileImageUrl = s3Service.uploadImgFile(imageFile, ImgType.PROFILE); + s3Service.deleteExProfile(principal.getName()); + return new DataResponse<>(profileImageUrl); + } + + @PostMapping("/gpa") + public CustomResponse uploadGpaImage(@RequestParam("imageFile") MultipartFile imageFile) { + ImageUrlDto profileImageUrl = s3Service.uploadImgFile(imageFile, ImgType.GPA); + return new DataResponse<>(profileImageUrl); + } + + @PostMapping("/language-test") + public CustomResponse uploadLanguageImage(@RequestParam("imageFile") MultipartFile imageFile) { + ImageUrlDto profileImageUrl = s3Service.uploadImgFile(imageFile, ImgType.LANGUAGE_TEST); + return new DataResponse<>(profileImageUrl); + } +} diff --git a/src/main/java/com/example/solidconnection/s3/S3Service.java b/src/main/java/com/example/solidconnection/s3/S3Service.java new file mode 100644 index 000000000..185400a3a --- /dev/null +++ b/src/main/java/com/example/solidconnection/s3/S3Service.java @@ -0,0 +1,105 @@ +package com.example.solidconnection.s3; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.SdkClientException; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.siteuser.service.SiteUserValidator; +import com.example.solidconnection.type.ImgType; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import static com.example.solidconnection.custom.exception.ErrorCode.*; + +@Service +@RequiredArgsConstructor +public class S3Service { + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + private final AmazonS3Client amazonS3; + private final SiteUserValidator siteUserValidator; + + public ImageUrlDto uploadImgFile(MultipartFile multipartFile, ImgType imageFile) { + validateImgFile(multipartFile); + String contentType = multipartFile.getContentType(); + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(contentType); + metadata.setContentLength(multipartFile.getSize()); + + UUID randomUUID = UUID.randomUUID(); + String fileName = imageFile.getType() + "/"+ randomUUID; + + try { + amazonS3.putObject(new PutObjectRequest(bucket, fileName, multipartFile.getInputStream(), metadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + } catch (AmazonServiceException e) { + e.printStackTrace(); + throw new CustomException(S3_SERVICE_EXCEPTION); + } catch (SdkClientException | IOException e) { + e.printStackTrace(); + throw new CustomException(S3_CLIENT_EXCEPTION); + } + + return new ImageUrlDto(amazonS3.getUrl(bucket, fileName).toString()); + } + + private void validateImgFile(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new CustomException(FILE_NOT_EXIST); + } + + String fileName = Objects.requireNonNull(file.getOriginalFilename()); + String fileExtension = getFileExtension(fileName).toLowerCase(); + + List allowedExtensions = Arrays.asList("jpg", "jpeg", "png"); + if (!allowedExtensions.contains(fileExtension)) { + throw new CustomException(NOT_IMG_FILE_EXTENSIONS, "허용된 형식: " + allowedExtensions); + } + } + + private String getFileExtension(String fileName) { + int dotIndex = fileName.lastIndexOf('.'); + if (dotIndex < 0 || dotIndex == fileName.length() - 1) { + throw new CustomException(INVALID_FILE_EXTENSIONS); + } + return fileName.substring(dotIndex + 1); + } + + public void deleteExProfile(String email){ + String key = getExProfileImageUrl(email); + deleteFile(key); + } + + private void deleteFile(String fileName) { + try { + amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName)); + } catch (AmazonServiceException e) { + e.printStackTrace(); + throw new CustomException(S3_SERVICE_EXCEPTION); + } catch (SdkClientException e) { + e.printStackTrace(); + throw new CustomException(S3_CLIENT_EXCEPTION); + } + } + + private String getExProfileImageUrl(String email){ + SiteUser siteUser = siteUserValidator.validateExistByEmail(email); + String fileName = siteUser.getProfileImageUrl(); + int domainStartIndex = fileName.indexOf(".com"); + return fileName.substring(domainStartIndex + 5); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java new file mode 100644 index 000000000..47143370a --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java @@ -0,0 +1,20 @@ +package com.example.solidconnection.siteuser.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; + +@Service +@RequiredArgsConstructor +public class SiteUserValidator { + private final SiteUserRepository siteUserRepository; + + public SiteUser validateExistByEmail(String email){ + return siteUserRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + } +} diff --git a/src/main/java/com/example/solidconnection/type/ImgType.java b/src/main/java/com/example/solidconnection/type/ImgType.java new file mode 100644 index 000000000..6f34ec267 --- /dev/null +++ b/src/main/java/com/example/solidconnection/type/ImgType.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.type; + +import lombok.Getter; + +@Getter +public enum ImgType { + PROFILE("profile"), GPA("gpa"), LANGUAGE_TEST("language"); + + private final String type; + + ImgType(String type){ + this.type = type; + } +}