엑셀 다운로드시 암호화를 하고 싶다면?
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
반응형