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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,6 +39,7 @@ public class AuthService {
private final RedisTemplate<String, String> 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;
Expand Down Expand Up @@ -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)){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand All @@ -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}")
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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); // 다음 필터로 요청과 응답 전달
Expand Down Expand Up @@ -72,9 +72,7 @@ private HashSet<String> 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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 파싱 에러"),
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/com/example/solidconnection/s3/AmazonS3Config.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/example/solidconnection/s3/ImageUrlDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.solidconnection.s3;

import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ImageUrlDto {
private String imageUrl;
}
45 changes: 45 additions & 0 deletions src/main/java/com/example/solidconnection/s3/S3Controller.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
105 changes: 105 additions & 0 deletions src/main/java/com/example/solidconnection/s3/S3Service.java
Original file line number Diff line number Diff line change
@@ -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<String> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/example/solidconnection/type/ImgType.java
Original file line number Diff line number Diff line change
@@ -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;
}
}