728x90
반응형
엑셀 파일을 만들고 업로드/다운로드 기능을 만들었다고 가정하자. 다운로드 된 엑셀에 보안 강화를 위해 암호화를 하고 싶을 수 있다. 암호화하는 방법을 간단하게 공유해본다. 환경은 아래와 같다.
- 엑셀 모듈
- 엑셀 기능이 커스터마이징된 모듈을 별도로 개발하는 것을 가정함
- Java + Gradle
- org.apache.poi (자세한 사항은 아래 dependencies 참고
// build.gradle
dependencies {
implementation 'org.jsoup:jsoup:1.15.3'
implementation group: "org.apache.poi", name: "poi", version: "4.1.2"
implementation group: "org.apache.poi", name: "poi-ooxml", version: "4.1.2"
}
- 애플리케이션
- 스프링부트(3.0.5) + 코틀린(1.7.22)
- 단순히 위 엑셀 모듈을 dependencies하여 사용하는 곳이다
단순한 엑셀 다운로드 기능
먼저 암호화 기능을 추가하기 전의 모습을 소개해본다.
public interface ExcelSupport {}
public class ExcelWriter {
private static final int DEFAULT_START_ROW = 0;
public <T extends ExcelSupport> String write(List<T> sources) {
return this.write(DEFAULT_START_ROW, sources);
}
public <T extends ExcelSupport> String write(int startRow, List<T> sources) {
if (sources.isEmpty()) throw new ExcelWriteException("sources must not be empty");
try (SXSSFWorkbook wb = new SXSSFWorkbook(SXSSFWorkbook.DEFAULT_WINDOW_SIZE)) {
wb.setCompressTempFiles(true);
SXSSFSheet sheet = wb.createSheet(sheetName);
CellStyle headerCellStyle = config.getHeaderStyle().defineCellStyle(wb);
initCellDateFormat(wb);
createHeader(startRow, headerCellStyle, sources.get(0).getClass(), sheet);
createRow(startRow, sources, sheet);
String path = getTempDir();
try (FileOutputStream out = new FileOutputStream(path)) {
wb.write(out);
out.close();
wb.dispose();
return path;
}
} catch (IOException | IllegalAccessException ex) {
throw new ExcelWriteException(ex);
}
}
}
위 코드를 보면 내부함수들도 보인다. 여기서 다룰 핵심 코드는 아니기에 아래에 접은글로 공유한다.
더보기
private void initCellDateFormat(SXSSFWorkbook wb) {
CellStyle dateCellStyle = wb.createCellStyle();
dateCellStyle.setDataFormat(wb.getCreationHelper().createDataFormat().getFormat(DateTypeNormalizer.DATE_FORMAT));
CellStyle dateTimeCellStyle = wb.createCellStyle();
dateTimeCellStyle.setDataFormat(wb.getCreationHelper().createDataFormat().getFormat(DateTypeNormalizer.DATETIME_FORMAT));
this.dateFormatStyles = Map.of("DATE", dateCellStyle, "DATETIME", dateTimeCellStyle);
}
private <T extends ExcelSupport> void createHeader(int startRow, CellStyle headerCellStyle, Class<T> clazz, SXSSFSheet sheet) {
Field[] fields = clazz.getDeclaredFields();
SXSSFRow row = sheet.createRow(startRow);
row.setHeight(config.getHeaderRowHeight());
for (Field field : fields) {
if (!field.isAnnotationPresent(ExcelExport.class)) continue;
ExcelExport ann = field.getAnnotation(ExcelExport.class);
SXSSFCell cell = row.createCell(ann.order());
cell.setCellValue(ann.name());
cell.setCellStyle(headerCellStyle);
}
}
private void createRow(int startRow, List<? extends ExcelSupport> sources, SXSSFSheet sheet)
throws IllegalAccessException {
int inputStartRow = startRow + 1;
for (ExcelSupport source : sources) {
Field[] fields = source.getClass().getDeclaredFields();
SXSSFRow row = sheet.createRow(inputStartRow++);
setCellValue(source, fields, row);
}
}
private static String getTempDir() {
String fileName = UUID.randomUUID().toString();
return DEFAULT_TEMP_DIR + "/" + fileName + ".xlsx";
}
실제 애플리케이션에서는 아래와 같이 사용하고 있다. 넥서스 레포지토리를 사용하여 모듈을 등록하고 이 모듈을 원하는 애플리케이션에서 사용하고 있다.
// build.gradle
dependencies {
// Excel Module. 존재하지 않는 라이브러리이다. 어디까지나 예시임
implementation("devvkkid", "excel-module", "1.2.0")
}
private fun convertToExcelResponse(result: List<UserResponse.Excel>, password: String): ExcelResponse {
val writer = ExcelWriter("샘플정보")
try {
val createdFilePath = writer.write(result, password)
val uploadFileName = fileManager.upload(createdFilePath)
val downloadFileName = "샘플정보-${LocalDateTimeFormatter.parseToTrimPattern(LocalDateTime.now())}"
return ExcelResponse(
fileManager.download(uploadFileName, downloadFileName), result.size
)
} catch (ex: ExcelWriteException) {
throw ExcelRuntimeException(ExcelRuntimeErrorCode.WRITE_ERROR, ex)
}
}
fileManager 정보는 여기서 핵심은 아니라 아래 접은글로 공유한다.
더보기
interface FileManager {
fun upload(filePath: String): String
fun upload(prefixPath: String, filePath: String): String
fun download(fileName: String, downloadFileName: String): String
fun getPresignedUrl(key: String, contentType: String, signatureDuration: Duration): String
}
@Component
class S3ExcelFileManagerClient(
private val s3Client: S3Client,
private val s3Presigner: S3Presigner,
) : FileManager {
override fun upload(filePath: String): String {
return this.putObject(filePath)
}
/**
* S3 Bucket으로 Object를 upload 합니다
*
* @param filePath originalFile Path
* @return 업로드한 fileName
*/
fun putObject(filePath: String, prefixPath: String? = null): String {
val objectKey = if (prefixPath.isNullOrBlank())
"$KEY_PREFIX/${LocalDate.now().format(dateFormatter)}/${Path(filePath).fileName}"
else
"$KEY_PREFIX/${LocalDate.now().format(dateFormatter)}/$prefixPath/${Path(filePath).fileName}"
val por = PutObjectRequest.builder()
.bucket(bucketName)
.key(objectKey)
.build()
try {
s3Client.putObject(por, RequestBody.fromBytes(getObjectFile(filePath)))
return objectKey
} catch (ex: RuntimeException) {
throw ExcelFileManagerException(FAILED_FILE_UPLOAD, ex)
} finally {
Path(filePath).deleteExisting()
}
}
}
암호화 방법
이제 암호화하는 방법을 공유해본다. 위에 ExcelWriter 클래스의 write 함수를 변경할 것이다. try/catch 구문이 있었는데 여기 내부에 아래와 같은 코드를 넣으면 된다.
try (SXSSFWorkbook wb = new SXSSFWorkbook(SXSSFWorkbook.DEFAULT_WINDOW_SIZE)) {
// 생략
try (FileOutputStream out = new FileOutputStream(path)) {
// 암호화 설정
POIFSFileSystem fs = new POIFSFileSystem();
EncryptionInfo info = new EncryptionInfo(EncryptionMode.agile);
Encryptor enc = info.getEncryptor();
enc.confirmPassword(password);
// 작성한 데이터를 암호화된 스트림에 저장
try (OutputStream encOut = enc.getDataStream(fs)) {
wb.write(encOut);
}
// 파일 저장
fs.writeFilesystem(out);
out.close();
wb.dispose();
return path;
}
} catch (IOException | IllegalAccessException | GeneralSecurityException ex) {
throw new ExcelWriteException(ex);
}
- POIFS : 마이크로소프트 오피스의 OLE 2 Compound document 파일 포맷을 읽고 쓰는 컴포넌트. 모든 오피스 파일 포맷은 OLE2 방식이므로 하위 모든 컴포넌트의 기반이 된다. (출처: 위키백과)
- EncryptionInfo : 사용할 암호 및 해싱 알고리즘을 지정하는 추가 매개변수를 제공
- Encryptor : 실제 암호화를 위해 암호나 Key를 입력받는 클래스
- 작성한 데이터를 암호화된 스트림에 저장하고, 파일에 바이트 스트림을 저장하는 과정을 잊으면 안된다.
자료 출처
- Apache POI - Encryption support
- 자바 POI 엑셀 다운로드 암호화
.
728x90
반응형
'Spring' 카테고리의 다른 글
application.yml에서 AWS 환경변수를 지워도 동작하는 이유 (0) | 2023.08.24 |
---|---|
내가 만든 라이브러리/모듈을 로컬에서 사용하기 (0) | 2023.08.07 |
AWS SQS와 EventBridge를 활용한 스케줄러 만들기 (0) | 2023.06.27 |
Mockito에서 only과 time(1)은 어떤 차이일까? (0) | 2023.05.07 |
스프링 JPA 환경 변수 중 몰랐던 것들 (0) | 2022.11.01 |
Comment