Keep going
파일 업로드 상세 처리 본문
22.1 파일의 확장자나 크기의 사전 처리
최근 포털에서도 특정한 확장자를 제외한 파일들의 업로드를 제한하는 경우가 많은데, 이는 첨부파일을 이용하는 웹 공격을 막기 위해서 행해지는 조치이다.
실습에서는 확장자가 (exe, sh, zip)등의 경우에는 업로드를 제한하고, 특정 크기 이상의 파일은 업로드할 수 없도록 제한하는 처리를 JavaScript로 처리한다.
uploadAjax.jsp에 파일의 확장자와 크기를 설정하고, 이를 검사하는 함수 checkExtension()을 작성해서 적용한다.
- uploadAjax.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Upload with Ajax</h1>
<div class='uploadDiv'>
<input type='file' name='uploadFile' multiple>
</div>
<button id='uploadBtn'> Upload </button>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script>
$(document).ready(function() {
var regex = new RegExp("(.*?)\.(exe|sh|zip|alz)$");
var maxSize = 5242880; //5MB
function checkExtension(fileName, fileSize) {
if(fileSize >= maxSize) {
alert("파일 사이즈 초과");
return false;
}
if(regex.test(fileName)) {
alert("해당 종류의 파일은 업로드할 수 없습니다.");
return false;
}
return true;
}
$("#uploadBtn").on("click", function(e){
var formData = new FormData();
var inputFile = $("input[name='uploadFile']");
var files = inputFile[0].files;
console.log(files);
//add filedate to formdata
for(var i=0; i<files.length; i++) {
if(!checkExtension(files[i].name, files[i].size)) {
return false;
}
formData.append("uploadFile", files[i]);
}
$.ajax({
url: '/uploadAjaxAction',
processData : false,
contentType: false,
data: formData,
type: 'POST',
success: function(result) {
alert("Uploaded");
}
});
});
});
</script>
</body>
</html>
|
cs |
첨부파일을 업로드하면 for 루프에서 checkExtension( )을 호출해서 확장자와 파일의 크기를 체크하게 된다.
-
중복된 이름의 첨부파일 처리
- 첨부파일을 저장할 때 신경 쓰이는 것은 크게 두가지로 1) 중복된 이름의 파일 처리와 2) 한 폴더 내에 너무 많은 파일의 생성문제다.
- 1)의 경우는 현재 시간을 밀리세컨드까지 구분해서 파일 이름을 생성해서 저장하거나 UUID를 이용해서 중복이 발생할 가능성이 거의 없는 문자열을 생성해서 처리한다.
- 2)의 경우는 하나의 폴더에 생성될 수 있는 파일의 개수에 대한 문제인데, 한 폴더에 너무 많은 파일이 이쓴 경우 속도와 저하와 개수의 제한 문제가 생기는 것을 방지해야 한다.
- 이에 대한 해결책으로 '년/월/일' 단위의 폴더를 생성해서 파일을 저장하는 것이다.
-
년/월/일 폴더의 생성
- 첨부파일을 보관하는 폴더를 생성하는 작업은 한 번에 폴더를 생성하거나 존재하는 폴더를 이용하는 방식을 사용한다.
- java.io.File에 존재하는 mkdirs()를 이용하면 필요한 상위 폴더까지 한 번에 생성할 수 있으므로 간단히 처리할 수 있다.
- UploadController
private String getFolder() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = new Date();
String str = sdf.format(date);
return str.replace("-", File.separator);
}
@PostMapping("/uploadAjaxAction")
public void uploadAjaxPost(MultipartFile[] uploadFile) {
log.info("update ajax post.........");
String uploadFolder = "C:\\upload";
//make folder
File uploadPath = new File(uploadFolder, getFolder());
log.info("upload path : " + uploadPath);
if(uploadPath.exists() ==false) {
uploadPath.mkdirs();
} //make folder
for(MultipartFile multipartFile : uploadFile) {
log.info("-------------");
log.info("Upload File Name : " + multipartFile.getOriginalFilename());
log.info("Upload File Size : " + multipartFile.getSize());
String uploadFileName = multipartFile.getOriginalFilename();
//IE has file path
uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1);
log.info("only file name : " + uploadFileName);
File saveFile = new File(uploadPath, uploadFileName);
try {
multipartFile.transferTo(saveFile);
}catch(Exception e) {
log.error(e.getMessage());
}
}
}
|
cs |
- getFolder()는 오는 날짜의 경로를 문자열로 생성한다. 생성된 경로는 폴더 경로로 수정된 뒤에 반환한다.
- uploadAjaxPost()에서는 해당 경로가 있는지 검사하고, 폴더를 생성한다. 이후 생성된 폴더로 파일을 저장하게 한다.
- 위와 같이 폴더를 생성한 후 uploadPath 경로에 파일을 저장헥 되면 자동으로 폴더가 생성되면서 파일이 저장되는 것을 볼 수 있다.
-
중복 방지를 위한 UUID 적용
- 파일 이름을 생성할 때 동일한 이름으로 업로드 되면 기존 파일을 지우게 되므로 java.util.UUID의 값을 이용해서 처리한다.
- UploadController
package org.zerock.controller;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.multipart.MultipartFile;
import lombok.extern.log4j.Log4j;
@Controller
@Log4j
public class UploadController {
@GetMapping("/uploadForm")
public void uploadForm() {
log.info("upload form");
}
@PostMapping("/uploadFormAction")
public void uploadFormPost(MultipartFile[] uploadFile, Model model) {
String uploadFolder = "C:\\upload";
for(MultipartFile multipartFile : uploadFile) {
log.info("--------------------");
log.info("Upload File Name : " + multipartFile.getOriginalFilename());
log.info("Upload File size : " + multipartFile.getSize());
File saveFile = new File(uploadFolder, multipartFile.getOriginalFilename());
try {
multipartFile.transferTo(saveFile);
} catch(Exception e) {
log.error(e.getMessage());;
}
}
}
@GetMapping("/uploadAjax")
public void uploadAjax() {
log.info("upload ajax");
}
private String getFolder() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = new Date();
String str = sdf.format(date);
return str.replace("-", File.separator);
}
@PostMapping("/uploadAjaxAction")
public void uploadAjaxPost(MultipartFile[] uploadFile) {
log.info("update ajax post.........");
String uploadFolder = "C:\\upload";
//make folder
File uploadPath = new File(uploadFolder, getFolder());
log.info("upload path : " + uploadPath);
if(uploadPath.exists() ==false) {
uploadPath.mkdirs();
} //make folder
for(MultipartFile multipartFile : uploadFile) {
log.info("-------------");
log.info("Upload File Name : " + multipartFile.getOriginalFilename());
log.info("Upload File Size : " + multipartFile.getSize());
String uploadFileName = multipartFile.getOriginalFilename();
//IE has file path
uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1);
log.info("only file name : " + uploadFileName);
UUID uuid = UUID.randomUUID();
uploadFileName = uuid.toString() + "_" + uploadFileName;
File saveFile = new File(uploadPath, uploadFileName);
try {
multipartFile.transferTo(saveFile);
}catch(Exception e) {
log.error(e.getMessage());
}
}
}
}
|
cs |
- 첨부파일은 randomUUID()를 이용해서 임의의 값을 생성한다.
- 생성된 값은 원래의 파일 이름과 구분할 수 있도록 중간에 '_'를 추가한다. 나중에 앞에서부터 '_'를 기준으로 분리하면 원래의 파일 이름을 파악할 수 있다.
22.2 섬네일 이미지 생성
- 일반 파일과 이미지 파일을 구분해야 한다.
- 이미지 파일의 경우에는 화면에 보여지는 작은 이미지를 생성하는 추가적인 처리이다.
- 만일 용량이 큰 파일을 섬네일 처리하지 않는다면 모바일과 같은 환경에서 많은 데이터를 소비해야만 하므로 이미지의 경우는 특별한 경우가 아니라면 섬네일을 제작해야만 한다.
섬네일 제작하는 방법
- ImageIO (JDK1.4부터)
- ImgScalr 라이브러리
- Thumbnailator 라이브러리 (실습에서 사용)
UploadController에서는 다음과 같은 단계를 이용해서 섬네일을 생성한다.
- 업로드된 파일이 이미지 종류의 파일인지 확인
- 이미지 파일의 경우에는 섬네일 이미지 생성 및 저장
porm.xml에 thumbnailator 라이브러리 포함
-
이미지 파일의 판단
- 화면에서 약간의 검사를 통해 업로드되는 파일의 확장자를 검사하기는 하지만, Ajax로 사용하는 호출은 반드시 브라우저만을 통해서 들어오는 것이 아니므로 확인할 필요가 있다.
- 서버에 업로드 된 파일은 조금 시간이 걸려도 파일 자체가 이미지인지를 정확히 체크한 후 저장하는 것이 좋다.
- checkImageType( ) 메서드
private boolean checkImageType(File file) {
try {
String contentType = Files.probeContentType(file.toPath());
return contentType.startsWith("image");
}catch(IOException e) {
e.printStackTrace();
}
return false;
}
|
cs |
- UserController
package org.zerock.controller;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
import lombok.extern.log4j.Log4j;
import net.coobird.thumbnailator.Thumbnailator;
@Controller
@Log4j
public class UploadController {
@GetMapping("/uploadForm")
public void uploadForm() {
log.info("upload form");
}
@PostMapping("/uploadFormAction")
public void uploadFormPost(MultipartFile[] uploadFile, Model model) {
String uploadFolder = "C:\\upload";
for(MultipartFile multipartFile : uploadFile) {
log.info("--------------------");
log.info("Upload File Name : " + multipartFile.getOriginalFilename());
log.info("Upload File size : " + multipartFile.getSize());
File saveFile = new File(uploadFolder, multipartFile.getOriginalFilename());
try {
multipartFile.transferTo(saveFile);
} catch(Exception e) {
log.error(e.getMessage());;
}
}
}
@GetMapping("/uploadAjax")
public void uploadAjax() {
log.info("upload ajax");
}
private String getFolder() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = new Date();
String str = sdf.format(date);
return str.replace("-", File.separator);
}
private boolean checkImageType(File file) {
try {
String contentType = Files.probeContentType(file.toPath());
return contentType.startsWith("image");
}catch(IOException e) {
e.printStackTrace();
}
return false;
}
@PostMapping("/uploadAjaxAction")
public void uploadAjaxPost(MultipartFile[] uploadFile) {
log.info("update ajax post.........");
String uploadFolder = "C:\\upload";
//make folder
File uploadPath = new File(uploadFolder, getFolder());
log.info("upload path : " + uploadPath);
if(uploadPath.exists() ==false) {
uploadPath.mkdirs();
} //make folder
for(MultipartFile multipartFile : uploadFile) {
log.info("-------------");
log.info("Upload File Name : " + multipartFile.getOriginalFilename());
log.info("Upload File Size : " + multipartFile.getSize());
String uploadFileName = multipartFile.getOriginalFilename();
//IE has file path
uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1);
log.info("only file name : " + uploadFileName);
UUID uuid = UUID.randomUUID();
uploadFileName = uuid.toString() + "_" + uploadFileName;
try {
File saveFile = new File(uploadPath, uploadFileName);
multipartFile.transferTo(saveFile);
//check image type file
if(checkImageType(saveFile)) {
FileOutputStream thumbnail = new FileOutputStream(new File(
uploadPath, "s_" + uploadFileName));
Thumbnailator.createThumbnail(multipartFile.getInputStream(),
thumbnail, 100, 100);
thumbnail.close();
}
}catch(Exception e) {
log.error(e.getMessage());
}
}
}
}
|
cs |
- Thumbinator는 InputStream과 java.io.File 객체를 이용해서 파일을 생성할 수 있고, 뒤에 사이즈에 대한 부분을 파라미터로 width와 height를 지정할 수 있다.
- 이제 /uploadAjaxx를 이용해서 이미지 파일을 업로드하면 원본 파일은 그대로 저장되고, 파일 이름이 's_'로 시작하는 섬네일 파일이 생성되는 것을 볼 수 있다. 반면 일반 파일의 경우는 그냥 파일만 업로드 되는 것을 볼 수 있다.
22.3 업로드된 파일의 데이터 반환
현재 Ajax 를 이용해서 파일을 업로드했지만, 아직 브라우저 쪽에 아무런 데이터도 전달하지 않았기 때문에 브라우저에서는 어떠한 피드백도 받을 수 없는 상황이다.
서버에서 Ajax이 결과로 전달해야 하는 데이터는 업로드된 파일의 경로가 포함된 파일의 이름이다.
섬네일 의 경우에는 's_'로 시작한다는 규칙만 알고 있으면 필요할 때 사용할 수 있다.
브라우저로 전송해야 하는 데이터는 다음과 같은 정보를 포함하도록 설계한다.
- 업로드된 파일의 이름과 원본 파일의 이름
- 파일이 저장된 경로
- 업로드된 파일이 이미지인지 아닌지에 대한 정보
이에 대한 모든 정보를 처리하는 방법은 1)업로드된 경로가 포함된 파일 이름을 반환하는 방식과 2)별도의 객체를 생성해서 처리하는 방법 을 고려할 수 있다.
por.xml에 jackson-databind 라이브러리 포함
-
AttachFileDTO 클래스
-
첨부파일의 정보들을 저장하는 클래다.
-
AttachFileDTO 클래스에는 원본 파일의 이름(fileName), 업로드 경로(uploadPath), UUID값(uuid), 이미지 여부(image) 정보를 하나로 묶어서 전달하는 용도로 사용한다.
-
package org.zerock.domain;
import lombok.Data;
@Data
public class AttachFileDTO {
private String fileName;
private String uploadPath;
private String uuid;
private boolean image;
}
|
cs |
UserController는 AttachFileDTO의 리스트를 반환하는 구조로 변경해야 한다.
- UploadController
@PostMapping(value="/uploadAjaxAction",
produces= MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public ResponseEntity<List<AttachFileDTO>>
uploadAjaxPost(MultipartFile[] uploadFile) {
List<AttachFileDTO> list = new ArrayList<AttachFileDTO>();
String uploadFolder= "C:\\upload";
String uploadFolderPath = getFolder();
//make folder
File uploadPath = new File(uploadFolder, uploadFolderPath);
if(uploadPath.exists()==false) {
uploadPath.mkdirs();
}
for(MultipartFile multipartFile : uploadFile) {
AttachFileDTO attachDTO = new AttachFileDTO();
String uploadFileName = multipartFile.getOriginalFilename();
uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1);
log.info("only file name : " + uploadFileName);
attachDTO.setFileName(uploadFileName);
UUID uuid = UUID.randomUUID();
uploadFileName = uuid.toString() + "_" + uploadFileName;
try {
File saveFile = new File(uploadPath, uploadFileName);
multipartFile.transferTo(saveFile);
attachDTO.setUuid(uuid.toString());
attachDTO.setUploadPath(uploadFolderPath);
if(checkImageType(saveFile)) {
attachDTO.setImage(true);
FileOutputStream thumbnail = new FileOutputStream(
new File(uploadPath, "s_" + uploadFileName));
Thumbnailator.createThumbnail(multipartFile.getInputStream(),
thumbnail, 100, 100);
thumbnail.close();
}
list.add(attachDTO);
} catch(Exception e) {
e.printStackTrace();
}
}
return new ResponseEntity<List<AttachFileDTO>>(list, HttpStatus.OK);
}
|
cs |
- uploadAjaxPost()는 기존과 달리 ResponseEntity<List<AttachFileDTO>>를 반환하는 형태로 수정하고, JSON 데이터를 반환하도록 변경된다.
- 내부에서는 각 파일에 맞게 AttachFileDTO를 생성해서 전달하는 구조로 변경된다.
-
브라우저에서 Ajax 처리
-
/uploadAjax에서 결과 데이터를 JavaScript를 이용해서 반환된 정보를 처리하도록 수정한다.
-
- uploadAjax.jsp
$.ajax({
url: '/uploadAjaxAction',
processData : false,
contentType: false,
data: formData,
type: 'POST',
dataType:'json',
success: function(result) {
console.log(result);
}
});
|
cs |
-Ajax를 호출했을 때의 결과 타입은 'json'으로 변경하고, 결과를 찍도록 변경했다.
출처 : 코드로 배우는 스프링 웹 프로젝트 [구멍가게 코딩단]
'Records > Spring Framework' 카테고리의 다른 글
브라우저에서 섬네일 처리 (0) | 2021.03.28 |
---|---|
파일 업로드 방식 (0) | 2021.03.28 |
댓글과 댓글 수에 대한 처리 (0) | 2021.03.26 |
스프링에서 트랜잭션 관리 (0) | 2021.03.22 |
AOP라는 패러다임 (0) | 2021.03.22 |