Skip to content

Commit

Permalink
perf(#173): Presigned URL 파일 형식 및 크기 검증
Browse files Browse the repository at this point in the history
- 증명사진, 원서 파일, 입학등록원 및 금연 서약서, 공지사항 파일을 업로드할 때 메타데이터를 전송하여 파일의 형식 및 크기를 검증할 수 있도록 하였습니다.
- 전송한 메타데이터와 다른 파일을 보낼 경우 S3 Bucket에서 차단하도록 Presigned URL을 개선했습니다.
  • Loading branch information
cabbage16 committed Feb 9, 2025
1 parent 7a4bb98 commit 5f35ed6
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.bamdoliro.maru.infrastructure.s3.FileService;
import com.bamdoliro.maru.infrastructure.s3.constants.FolderConstant;
import com.bamdoliro.maru.presentation.form.dto.response.AdmissionAndPledgeUrlResponse;
import com.bamdoliro.maru.presentation.form.dto.response.FormUrlResponse;
import com.bamdoliro.maru.shared.annotation.UseCase;
import lombok.RequiredArgsConstructor;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@
import com.bamdoliro.maru.domain.user.domain.User;
import com.bamdoliro.maru.infrastructure.s3.FileService;
import com.bamdoliro.maru.infrastructure.s3.constants.FolderConstant;
import com.bamdoliro.maru.infrastructure.s3.dto.request.FileMetadata;
import com.bamdoliro.maru.infrastructure.s3.dto.response.UrlResponse;
import com.bamdoliro.maru.infrastructure.s3.exception.FileSizeLimitExceededException;
import com.bamdoliro.maru.infrastructure.s3.exception.MediaTypeMismatchException;
import com.bamdoliro.maru.shared.annotation.UseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;

import static com.bamdoliro.maru.shared.constants.FileConstant.MB;

@RequiredArgsConstructor
@UseCase
Expand All @@ -17,11 +23,19 @@ public class UploadAdmissionAndPledgeUseCase {
private final FileService fileService;
private final FormFacade formFacade;

public UrlResponse execute(User user) {
public UrlResponse execute(User user, FileMetadata fileMetadata) {
Form form = formFacade.getForm(user);
validate(form);

return fileService.getPresignedUrl(FolderConstant.ADMISSION_AND_PLEDGE, user.getUuid().toString());
return fileService.getPresignedUrl(FolderConstant.ADMISSION_AND_PLEDGE, user.getUuid().toString(), fileMetadata, metadata -> {
if (!metadata.getMediaType().equals(MediaType.APPLICATION_PDF)) {
throw new MediaTypeMismatchException();
}

if (metadata.getFileSize() > 20 * MB) {
throw new FileSizeLimitExceededException();
}
});
}

private void validate(Form form) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,31 @@
import com.bamdoliro.maru.domain.user.domain.User;
import com.bamdoliro.maru.infrastructure.s3.FileService;
import com.bamdoliro.maru.infrastructure.s3.constants.FolderConstant;
import com.bamdoliro.maru.infrastructure.s3.dto.request.FileMetadata;
import com.bamdoliro.maru.infrastructure.s3.dto.response.UrlResponse;
import com.bamdoliro.maru.infrastructure.s3.exception.FileSizeLimitExceededException;
import com.bamdoliro.maru.infrastructure.s3.exception.MediaTypeMismatchException;
import com.bamdoliro.maru.shared.annotation.UseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;

import static com.bamdoliro.maru.shared.constants.FileConstant.MB;

@RequiredArgsConstructor
@UseCase
public class UploadFormUseCase {

private final FileService fileService;

public UrlResponse execute(User user) {
return fileService.getPresignedUrl(FolderConstant.FORM, user.getUuid().toString());
public UrlResponse execute(User user, FileMetadata fileMetadata) {
return fileService.getPresignedUrl(FolderConstant.FORM, user.getUuid().toString(), fileMetadata, metadata -> {
if (!metadata.getMediaType().equals(MediaType.APPLICATION_PDF)) {
throw new MediaTypeMismatchException();
}

if (metadata.getFileSize() > 20 * MB) {
throw new FileSizeLimitExceededException();
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,31 @@
import com.bamdoliro.maru.domain.user.domain.User;
import com.bamdoliro.maru.infrastructure.s3.FileService;
import com.bamdoliro.maru.infrastructure.s3.constants.FolderConstant;
import com.bamdoliro.maru.infrastructure.s3.dto.request.FileMetadata;
import com.bamdoliro.maru.infrastructure.s3.dto.response.UrlResponse;
import com.bamdoliro.maru.infrastructure.s3.exception.FileSizeLimitExceededException;
import com.bamdoliro.maru.infrastructure.s3.exception.MediaTypeMismatchException;
import com.bamdoliro.maru.shared.annotation.UseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;

import static com.bamdoliro.maru.shared.constants.FileConstant.MB;

@RequiredArgsConstructor
@UseCase
public class UploadIdentificationPictureUseCase {

private final FileService fileService;

public UrlResponse execute(User user) {
return fileService.getPresignedUrl(FolderConstant.IDENTIFICATION_PICTURE, user.getUuid().toString());
public UrlResponse execute(User user, FileMetadata fileMetadata) {
return fileService.getPresignedUrl(FolderConstant.IDENTIFICATION_PICTURE, user.getUuid().toString(), fileMetadata, metadata -> {
if (!(metadata.getMediaType().equals(MediaType.IMAGE_PNG) || metadata.getMediaType().equals(MediaType.IMAGE_JPEG))) {
throw new MediaTypeMismatchException();
}

if (metadata.getFileSize() > 2 * MB) {
throw new FileSizeLimitExceededException();
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.bamdoliro.maru.infrastructure.s3.FileService;
import com.bamdoliro.maru.infrastructure.s3.constants.FolderConstant;
import com.bamdoliro.maru.presentation.notice.dto.request.UploadFileRequest;
import com.bamdoliro.maru.infrastructure.s3.dto.request.FileMetadata;
import com.bamdoliro.maru.presentation.notice.dto.response.UploadFileResponse;
import com.bamdoliro.maru.shared.annotation.UseCase;
import lombok.RequiredArgsConstructor;
Expand All @@ -15,16 +15,23 @@ public class UploadFileUseCase {

private final FileService fileService;

public List<UploadFileResponse> execute(UploadFileRequest request) {
List<String> fileNameList = request.getFileNameList().stream()
.map(fileName -> UUID.randomUUID() + "_" + fileName)
.toList();
public List<UploadFileResponse> execute(List<FileMetadata> metadataList) {
validate(metadataList);

return fileNameList.stream()
.map(fileName -> new UploadFileResponse(
fileService.getPresignedUrl(FolderConstant.NOTICE_FILE, fileName),
fileName
))
return metadataList.stream()
.map(metadata1 -> {
String fileName = UUID.randomUUID() + "_" + metadata1.getFileName();
return new UploadFileResponse(
fileService.getPresignedUrl(FolderConstant.NOTICE_FILE, fileName, metadata1, metadata2 -> {}),
fileName
);
})
.toList();
}

public void validate(List<FileMetadata> metadataList) {
if (metadataList.isEmpty() || metadataList.size() > 3) {
throw new IllegalArgumentException("metadataList must contain 1~3 elements");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import com.bamdoliro.maru.infrastructure.s3.dto.response.UrlResponse;
import com.bamdoliro.maru.infrastructure.s3.exception.FailedToSaveException;
import com.bamdoliro.maru.infrastructure.s3.validator.FileValidator;
import com.bamdoliro.maru.infrastructure.s3.dto.request.FileMetadata;
import com.bamdoliro.maru.shared.config.properties.S3Properties;
import lombok.RequiredArgsConstructor;
import org.apache.http.entity.ContentType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
Expand All @@ -29,9 +29,10 @@ public class FileService {
@Value("${spring.cloud.aws.s3.bucket}")
private String bucket;

@Deprecated

/** @deprecated */
public UrlResponse execute(MultipartFile file, String folder, String fileName, FileValidator validator) {
validator.validate(file);
// validator.validate(file);
String fullFileName = createFileName(folder, fileName);

try {
Expand All @@ -53,9 +54,10 @@ public UrlResponse execute(MultipartFile file, String folder, String fileName, F
);
}

public String getUploadPresignedUrl(String folder, String fileName) {
public String getUploadPresignedUrl(String folder, String fileName, FileMetadata fileMetadata, FileValidator validator) {
validator.validate(fileMetadata);
String fullFileName = createFileName(folder, fileName);
GeneratePresignedUrlRequest request = getGenerateUploadPresignedUrlRequest(bucket, fullFileName);
GeneratePresignedUrlRequest request = getGenerateUploadPresignedUrlRequest(bucket, fullFileName, fileMetadata);

return amazonS3Client.generatePresignedUrl(request).toString();
}
Expand All @@ -67,19 +69,20 @@ public String getDownloadPresignedUrl(String folder, String fileName) {
return request != null ? amazonS3Client.generatePresignedUrl(request).toString() : null;
}

public UrlResponse getPresignedUrl(String folder, String fileName) {
public UrlResponse getPresignedUrl(String folder, String fileName, FileMetadata request, FileValidator validator) {
return new UrlResponse(
getUploadPresignedUrl(folder, fileName),
getUploadPresignedUrl(folder, fileName, request, validator),
getDownloadPresignedUrl(folder, fileName)
);
}

private GeneratePresignedUrlRequest getGenerateUploadPresignedUrlRequest(String bucket, String fileName) {
private GeneratePresignedUrlRequest getGenerateUploadPresignedUrlRequest(String bucket, String fileName, FileMetadata fileMetadata) {
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucket, fileName)
.withMethod(HttpMethod.PUT)
.withExpiration(getPresignedUrlExpiration(3));

request.putCustomRequestHeader(Headers.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString());
request.putCustomRequestHeader(Headers.CONTENT_TYPE, fileMetadata.getMediaType().toString());
request.putCustomRequestHeader(Headers.CONTENT_LENGTH, fileMetadata.getFileSize().toString());

return request;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.bamdoliro.maru.infrastructure.s3.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.http.MediaType;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class FileMetadata {

@NotBlank(message = "필수값입니다.")
private String fileName;

@NotNull(message = "필수값입니다.")
private MediaType mediaType;

@NotNull(message = "필수값입니다.")
private Long fileSize;
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package com.bamdoliro.maru.infrastructure.s3.validator;

import com.bamdoliro.maru.infrastructure.s3.exception.EmptyFileException;
import org.springframework.web.multipart.MultipartFile;
import com.bamdoliro.maru.infrastructure.s3.dto.request.FileMetadata;

@FunctionalInterface
public interface FileValidator {
void customValidate(MultipartFile file);
void customValidate(FileMetadata request);

default void validate(MultipartFile file) {
if (file.isEmpty()) {
default void validate(FileMetadata request) {
if (request.getFileName().isBlank() || request.getFileName().lastIndexOf(".") == -1) {
throw new EmptyFileException();
}

customValidate(file);
customValidate(request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.bamdoliro.maru.presentation.form.dto.request.PassOrFailFormListRequest;
import com.bamdoliro.maru.presentation.form.dto.request.SubmitFormRequest;
import com.bamdoliro.maru.presentation.form.dto.request.UpdateFormRequest;
import com.bamdoliro.maru.infrastructure.s3.dto.request.FileMetadata;
import com.bamdoliro.maru.presentation.form.dto.response.*;
import com.bamdoliro.maru.shared.auth.AuthenticationPrincipal;
import com.bamdoliro.maru.shared.auth.Authority;
Expand All @@ -20,7 +21,6 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand Down Expand Up @@ -168,29 +168,29 @@ public void updateForm(
@ResponseStatus(HttpStatus.CREATED)
@PostMapping( "/identification-picture")
public SingleCommonResponse<UrlResponse> uploadIdentificationPicture(
@AuthenticationPrincipal(authority = Authority.USER) User user
@AuthenticationPrincipal(authority = Authority.USER) User user,
@RequestBody @Valid FileMetadata metadata
) {
return SingleCommonResponse.ok(
uploadIdentificationPictureUseCase.execute(user)
uploadIdentificationPictureUseCase.execute(user, metadata)
);
}

@ResponseStatus(HttpStatus.CREATED)
@PostMapping("/form-document")
public SingleCommonResponse<UrlResponse> uploadFormDocument(
@AuthenticationPrincipal(authority = Authority.USER) User user
@AuthenticationPrincipal(authority = Authority.USER) User user,
@RequestBody @Valid FileMetadata metadata
) {
return SingleCommonResponse.ok(
uploadFormUseCase.execute(user)
uploadFormUseCase.execute(user, metadata)
);
}

@GetMapping("/export")
public ResponseEntity<Resource> exportForm(
@AuthenticationPrincipal(authority = Authority.USER) User user,
Model model
@AuthenticationPrincipal(authority = Authority.USER) User user
) {
model.addAttribute("a", "a");
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
.body(exportFormUseCase.execute(user));
Expand All @@ -207,10 +207,11 @@ public ResponseEntity<Resource> downloadAdmissionAndPledgeFormat(

@PostMapping("/admission-and-pledge")
public SingleCommonResponse<UrlResponse> uploadAdmissionAndPledge(
@AuthenticationPrincipal(authority = Authority.USER) User user
@AuthenticationPrincipal(authority = Authority.USER) User user,
@RequestBody FileMetadata metadata
) {
return SingleCommonResponse.ok(
uploadAdmissionAndPledgeUseCase.execute(user)
uploadAdmissionAndPledgeUseCase.execute(user, metadata)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import com.bamdoliro.maru.application.notice.*;
import com.bamdoliro.maru.domain.user.domain.User;
import com.bamdoliro.maru.infrastructure.s3.dto.request.FileMetadata;
import com.bamdoliro.maru.presentation.notice.dto.request.NoticeRequest;
import com.bamdoliro.maru.presentation.notice.dto.request.UploadFileRequest;
import com.bamdoliro.maru.presentation.notice.dto.response.NoticeResponse;
import com.bamdoliro.maru.presentation.notice.dto.response.NoticeSimpleResponse;
import com.bamdoliro.maru.presentation.notice.dto.response.UploadFileResponse;
Expand All @@ -18,6 +18,8 @@
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequiredArgsConstructor
@RequestMapping("/notice")
@RestController
Expand All @@ -43,10 +45,10 @@ public SingleCommonResponse<IdResponse> createNotice(
@PostMapping("/file")
public ListCommonResponse<UploadFileResponse> uploadFile(
@AuthenticationPrincipal(authority = Authority.ADMIN) User user,
@RequestBody @Valid UploadFileRequest request
) {
@RequestBody @Valid List<FileMetadata> metadataList
) {
return SingleCommonResponse.ok(
uploadFileUseCase.execute(request)
uploadFileUseCase.execute(metadataList)
);
}

Expand Down

This file was deleted.

0 comments on commit 5f35ed6

Please sign in to comment.