파일 업로드 기반의 기능을 종종 개발해야할 일이 있습니다.
이때 업로드 파일의 유효성(특히, 보안문제로)을 잘 진행해줘야합니다.
간단하게 범용적으로 사용할만한 커스텀 어노테이션을 만들어서 메모 목적으로 글을 작성해둡니다.
* 요약: 파일명의 확장자 검사뿐만 아니라, 확장자가 수정된 파일인지도 잘 검사해줘야함
1. apache tika 라이브러리를 추가해서 mime타입 검사에 사용
<!-- 업로드 파일의 확장자 검사 등의 목적 -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.8.0</version>
</dependency>
2. 업로드 허용 파일들에 대해서 정의 enum 작성
- 코드 관리를 조금 더 타이트하게 하려고 정의했는데 안쓰고 구현해도 무방
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 업로드 허용 파일에 대한 정의 Enum
* - 실제 사용되는 일부 파일들에 대한 정의만 추가되어 있으니, 신규 정의가 필요하면 내용 작성해서 사용
*
* @author
*/
@Getter
@AllArgsConstructor
public enum UploadAllowFileDefine {
// @formatter:off
CSV("csv", new String[]{"text/csv", "text/plain"}), //텍스트 에디터에서 수정되는 text/plain 도 허용함
;
// @formatter:on
private String fileExtensionLowerCase; //파일 확장자(소문자)
private String[] allowMimeTypes; //허용하는 mime type array(파일 내용 변조 후 확장자 변경하는 공격을 막기 위해서 사용. 2023-07-31 기준 apache TIKA로 detect 중)
}
3. 커스텀 valid 인터페이스 작성
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 업로드 파일의 유효성 검사 체크용
*
* @author
*/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = FileUploadValidator.class)
public @interface FileUploadValid {
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/** 업로드 허용 파일들의 정의 array(여러 종류의 파일 타입을 허용할 수도 있기에 array) */
UploadAllowFileDefine[] allowFileDefines();
}
4. 커스텀 validator 구현체 작성
....
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.tika.Tika;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.io.IOException;
/**
* 업로드 파일의 유효성 검사 체크용
* - 사용 방법 예) @FileUploadValid(allowFileDefines = {UploadAllowFileDefine.CSV}, message = "유효한 CSV파일만 업로드 가능합니다.")
*
* @author
*/
@Slf4j
public class FileUploadValidator implements ConstraintValidator<FileUploadValid, MultipartFile> {
private FileUploadValid annotation;
@Override
public void initialize(FileUploadValid constraintAnnotation) {
this.annotation = constraintAnnotation;
}
@Override
public boolean isValid(MultipartFile multipartFile, ConstraintValidatorContext context) {
if (multipartFile.isEmpty()) {
context.buildConstraintViolationWithTemplate("업로드 대상 파일이 없습니다. 정확히 선택 업로드해주세요.(There is no file to upload. Please upload correctly)").addConstraintViolation();
return false;
}
final String fileName = multipartFile.getOriginalFilename();
if (StringUtils.isBlank(fileName)) {
context.buildConstraintViolationWithTemplate("업로드 요청한 파일명이 존재하지 않습니다.(not exist file name)").addConstraintViolation();
return false;
}
try {
int targetByte = multipartFile.getBytes().length;
if (targetByte == 0) {
context.buildConstraintViolationWithTemplate("파일의 용량이 0 byte입니다.(The size of the file is 0 bytes.)").addConstraintViolation();
return false;
}
} catch (IOException e) {
log.error(e.getMessage(), e);
context.buildConstraintViolationWithTemplate("파일의 용량 확인 중 에러가 발생했습니다.(An error occurred while checking the file size.)").addConstraintViolation();
return false;
}
//허용된 파일 확장자 검사
final String detectedMediaType = this.getMimeTypeByTika(multipartFile); //확장자 변조한 파일인지 확인을 위한 mime type 얻기
final UploadAllowFileDefine[] allowExtArray = annotation.allowFileDefines();
final String fileExt = FilenameUtils.getExtension(fileName);
for (UploadAllowFileDefine allowDefine : allowExtArray) {
//파일명의 허용 확장자 검사
if (StringUtils.equals(allowDefine.getFileExtensionLowerCase(), fileExt.toLowerCase()) == false) {
StringBuilder sb = new StringBuilder();
sb.append("허용되지 않는 확장자의 파일이며 다음 확장자들만 허용됩니다. This is a file with a disallowed extension, and only the following extensions are allowed.");
sb.append(": ");
sb.append(ArrayUtils.toString(allowExtArray));
context.buildConstraintViolationWithTemplate(sb.toString()).addConstraintViolation();
return false;
}
//파일 변조 업로드를 막기위한 mime타입 검사(예. exe파일을 csv로 확장자 변경하는 업로드를 막음)
if (ArrayUtils.contains(allowDefine.getAllowMimeTypes(), detectedMediaType) == false) {
StringBuilder sb = new StringBuilder();
sb.append("확장자 변조 파일은 허용되지 않습니다.(Modified files with extensions are not allowed.)");
context.buildConstraintViolationWithTemplate(sb.toString()).addConstraintViolation();
return false;
}
}
return true;
}
/**
* apache Tika라이브러리를 이용해서 파일의 mimeType을 가져옴
*
* @param multipartFile
* @return
*/
private String getMimeTypeByTika(MultipartFile multipartFile) {
try {
Tika tika = new Tika();
String mimeType = tika.detect(multipartFile.getInputStream());
log.debug("업로드 요청된 파일 {}의 mimeType:{}", multipartFile.getOriginalFilename(), mimeType);
return mimeType;
} catch (IOException e) {
log.error(e.getMessage(), e);
return null;
}
}
}
사용 예
@FileUploadValid(allowFileDefines = {UploadAllowFileDefine.CSV}, message = "유효한 CSV파일만 업로드 가능합니다.")
private MultipartFile targetCsvFile;
'JAVA > Spring 일반' 카테고리의 다른 글
spring webflux의 webclient 사용할 때 예외 핸들링 및 재 요청 샘플 (0) | 2023.11.16 |
---|---|
JPA N+1 문제 해결과 관련 Spring Data JDBC에서 기능 추가 예정과 관련 (0) | 2023.09.01 |
Spring에서 ModelAndViewDefiningException 및 상황에 따른 view 또는 json 응답 (0) | 2022.05.03 |
mysql + mybatis + Spring 조합에서 데이터 존재하는지 판다는하는 소스와 쿼리(성능 감안) (0) | 2022.04.08 |
자주 사용하는 Spring valid관련 내용들 - 메모 (0) | 2022.04.05 |