Apache Fesod 工作簿密码保护实战:读写加密 Excel 的完整指南

原创2026-10-03 09:03:26799 阅读
文章标签:后端

Apache Fesod 工作簿密码保护实战:读写加密 Excel 的完整指南

本文基于 Apache Fesod(Incubating)的官方文档 website/docs/sheet/advanced/password.md,讲解如何通过 FesodSheet 的 builder 链为 .xlsx / .xls 文件配置读写密码,并结合仓库源码剖析 .xlsx(AES 加密)与 .xls(BIFF8 写保护/加密)两条底层实现路径的差异。读完本文,你将能写出"一行配置即可加密"的读写代码,理解加密过程中内存占用偏高的原因,并掌握密码错误的异常处理与生产环境安全实践。

概述:Fesod 对 Excel 密码保护的支持方式

Fesod 完整支持 Excel 内置的密码保护能力,读写两个方向都覆盖:

  • 写入方向:通过 FesodSheet.write(...) 返回的 ExcelWriterBuilder 调用 .password("xxx"),写出加密文件;
  • 读取方向:通过 FesodSheet.read(...) 返回的 ExcelReaderBuilder 调用 .password("xxx"),解密并读取加密文件。

API 层面两者对称且极简,但底层实现针对不同文件格式有本质区别,这是使用前必须建立的认知:

  • 对 .xlsx(OOXML)输出,Fesod 基于 Apache POI 的标准加密方案(EncryptionMode.standard,AES 系列算法)对整份文件做内容级加密。由于该过程会把整个工作簿读入内存再重写,大工作簿加密时内存占用会显著升高(详见 ExcelWriterBuilder.java 中 password(...) 的 Javadoc 警告:"Encryption is when the entire file is read into memory, so it is very memory intensive")。
  • 对传统 .xls(BIFF8)文件,密码保护走的是 POI 的 Biff8EncryptionKey 机制,其行为更接近"写保护/BIFF8 记录级加密",与 .xlsx 的整文件内容加密在行为与安全保证上并不等价。仓库中的 EncryptDataTest.java 专门验证了:即使 .xls 写入密码后,未提供正确密码时读取会抛 EncryptedDocumentException,说明其内容同样受到 BIFF8 记录级加密保护,而非仅打上"只读"标记。

写入带密码的工作簿

完整代码示例

@Test
public void passwordWrite() {
    String fileName = "passwordWrite" + System.currentTimeMillis() + ".xlsx";

    FesodSheet.write(fileName)
        .password("your_password")
        .head(DemoData.class)
        .sheet("PasswordSheet")
        .doWrite(data());
}

要点说明:

  • .password("your_password") 在 ExcelWriterBuilder 上设置 WriteWorkbook 的密码字段,见 ExcelWriterBuilder.java,它调用 writeWorkbook.setPassword(password) 后返回当前 builder,因此可以继续链式调用 .head(...)、.sheet(...);
  • .head(DemoData.class) 声明表头模型,.sheet("PasswordSheet") 指定工作表名称,.doWrite(data()) 执行写出;
  • 文件扩展名 .xlsx 决定走 AES 加密路径;若把文件名改为 .xls,则走 BIFF8 路径(见下文格式差异)。

源码层面的加密时机与实现

写出流程最终由 WriteContextImpl.java 统一收口,加密发生在工作簿内容写完之后的收尾阶段,分两种通道:

1. XLSX 文件通道(doFileEncrypt07,第 699-714 行) 该方法先做两个前置判断:密码为空或文件类型不是 XLSX 时直接返回;文件对象为空也直接返回。随后调用 openFileSystemAndEncrypt(...)(第 727-736 行)完成加密:

POIFSFileSystem fileSystem = new POIFSFileSystem();
Encryptor encryptor = new EncryptionInfo(EncryptionMode.standard).getEncryptor();
encryptor.confirmPassword(writeWorkbookHolder.getPassword());
try (OPCPackage opcPackage = OPCPackage.open(file, PackageAccess.READ_WRITE);
        OutputStream outputStream = encryptor.getDataStream(fileSystem)) {
    opcPackage.save(outputStream);
}
return fileSystem;

这段代码揭示了 .xlsx 加密的核心原理:先以 EncryptionMode.standard(AES)创建 Encryptor 并确认密码,再以读写模式打开已写好的 OOXML 包,把包内容整体写入加密数据流。注意 OPCPackage.open(...) 与 opcPackage.save(...) 意味着整个工作簿内容都要驻留内存——这就是官方 Javadoc 明确警告"加密非常吃内存"的根源。

2. XLSX 输出流通道(doOutputStreamEncrypt07,第 663-694 行) 当目标是 OutputStream 而非文件时,加密同样先落地一个临时 .xlsx 文件(FileUtils.createTmpFile(...)),再复用同一个 openFileSystemAndEncrypt 流程加密,最后 fileSystem.writeFilesystem(outputStream) 写入目标流。因此无论文件通道还是流通道,xlsx 加密的内存成本是一致的。

3. XLS 通道(clearEncrypt03,第 649-655 行) 对 .xls 类型,Fesod 走 POI 的 Biff8EncryptionKey 线程本地密钥机制,写出后会把 Biff8EncryptionKey.setCurrentUserPassword(null) 清空,防止密钥残留污染后续写出。配套的 WorkBookUtilTest.java 验证了:createWorkBook() 之后 BIFF8 加密密码必须继续保持设置,直到文件最终写出完毕,这确保了 HSSFWorkbook.write() 阶段能拿到正确的密钥。

读取带密码的工作簿

完整代码示例

@Test
public void passwordRead() {
    String fileName = "path/to/encrypted.xlsx";

    FesodSheet.read(fileName, DemoData.class, new DemoDataListener())
        .password("your_password")
        .sheet()
        .doRead();
}

要点说明:

  • .password("your_password") 在 ExcelReaderBuilder 上设置 ReadWorkbook 的密码字段,见 ExcelReaderBuilder.java,它调用 readWorkbook.setPassword(password);
  • DemoData.class 是行数据模型,DemoDataListener 实现 ReadListener 逐行回调处理数据;.sheet() 进入 sheet 配置阶段,.doRead() 触发流式(SAX)解析。

读取端的格式嗅探与解密分流

读取加密文件时,Fesod 的入口分析器 ExcelAnalyserImpl.java 在 chooseExcelExecutor 中会先做一次"嗅探":

  • 先用 POIFSFileSystem 打开文件,判断其根目录是否含有 Decryptor.DEFAULT_POIFS_ENTRY(这是加密 OOXML 包的标准入口标记);
  • 若命中,说明"外表像 .xls、内里其实是加密的 .xlsx",此时调用 DocumentFactoryHelper.getDecryptedStream(poifsFileSystem.getRoot().getFileSystem(), readWorkbook.getPassword()) 解密出明文流,并改走 XlsxSaxAnalyser 按 XLSX 处理(第 137-153 行);
  • 若未命中,才真正按 .xls 处理,交给 XlsSaxAnalyser。

XlsxSaxAnalyser 的构造函数(XlsxSaxAnalyser.java)接收这个 decryptedStream,通过 readOpcPackage 打开 OPCPackage 后继续常规的 SAX 流式解析。

而 .xls 的读取则由 XlsSaxAnalyser.java 处理:setCurrentUserPassword() 从 readWorkbookHolder 取出密码并调用 Biff8EncryptionKey.setCurrentUserPassword(password) 注入 POI 的 HSSF 解密栈,解析结束后在 finally 中通过 clearCurrentUserPassword() 将密钥清空,避免线程残留。值得注意的是,加密 XLSX 走 XlsxSaxAnalyser 时不会注入 Biff8EncryptionKey,两者机制互相独立。

密码错误的典型表现

根据官方文档与 EncryptDataTest.java 的断言,不提供或提供错误密码时,读取会抛出 org.apache.poi.EncryptedDocumentException:

Assertions.assertThrows(EncryptedDocumentException.class, () ->
        FesodSheet.read(file, SimpleData.class, listener)
                .excelType(ExcelTypeEnum.XLS)
                .sheet()
                .doReadSync());

该测试同时证明了 .xls 场景下"无密码必失败、正确密码必成功"的完整闭环(第 116-140 行),是验证加密行为最直接的仓库证据。

加密读写的全组合回归测试

仓库在 EncryptDataTest.java 中用参数化测试覆盖了加密读写的全部组合:

维度 取值
输入形式 文件(File)/ 输出流(Stream)
是否显式指定类型 不指定 / 显式指定 excelType
是否带密码 无密码 / 带密码(PASSWORD = "123456")

测试逻辑(readAndWrite)体现了两个实战要点:

  1. 流式 API 必须显式声明类型:当基于 InputStream/OutputStream 读写且未显式指定 excelType 时,需要调用 .excelType(ExcelTypeEnum.XLSX) 等手动指定,否则无法从流中可靠嗅探格式(源码注释 "Stream API needs type hint when not explicitly set");
  2. 读写密码必须一致:excelWriterBuilder.password(PASSWORD) 与 readerBuilder.password(PASSWORD) 配对使用,写 10 行数据后 doReadSync() 必须完整读回 10 行且字段非空。

安全注意事项与生产实践

官方文档对密码保护的安全语义与工程实践给出三条明确建议:

  1. 加密的是文件内容本身:Excel 密码保护会加密文件内容,没有密码的人无法读取文件内部数据,而不是简单的"禁止修改"标记;
  2. 密码错误必然抛异常:缺少正确密码时读取会抛出 EncryptedDocumentException,因此业务代码应在调用 .doRead() 的地方捕获该异常,给出"密码错误或文件已加密"的友好提示,避免堆栈直接暴露给终端用户;
  3. 禁止硬编码密码:生产环境中不要把密码写死在代码或配置仓库里,应使用配置文件(如环境变量、application.yml 外部化配置)、密钥管理服务(KMS / Vault)等机制动态注入,同时结合仓库 EncryptDataTest.java 的做法,用固定测试密码仅用于自动化验证。

此外,结合源码还可以补充两条官方文档未展开、但直接影响落地选型的结论:

  • .xlsx 加密的内存代价是刚性的:因为 OPCPackage.open(...) + save(...) 需要整包驻留内存,加密大文件前应评估 JVM 堆(-Xmx),必要时搭配 large-file.md 中介绍的流式读写策略,把"加密 + 超大文件"这类组合场景单独压测;
  • .xls 与 .xlsx 的安全语义不同:.xlsx 是标准的 AES 内容加密,.xls 是 BIFF8 记录级加密(POI Biff8EncryptionKey 机制)。虽然 EncryptDataTest.java 证明 .xls 也是"真加密"而非仅写保护,但两者加密强度与行为仍有差异,对安全敏感场景应优先使用 .xlsx。

结语

Apache Fesod 的密码保护 API 只有一行 .password("xxx"),但底层为 .xlsx(EncryptionMode.standard AES 加密、整包内存重写)与 .xls(BIFF8 密钥注入)设计了完全不同的两条加密链路,并在读取端通过 Decryptor.DEFAULT_POIFS_ENTRY 嗅探实现"加密 XLSX 伪装 XLS"的透明解密。本文的代码示例可直接复制运行,源码路径(ExcelWriterBuilder.java、ExcelReaderBuilder.java、WriteContextImpl.java、ExcelAnalyserImpl.java、EncryptDataTest.java)可供继续深入阅读。

登录后查看全文
fesod