!51 实现预览加密的(受密码保护)office文件

* 1. 修复getCorsFile接口高危安全漏洞
* 1. 优化密码错误提示(“密码错误,请重新输入密码。”)
* 1. 修复PPT重复预览bug,此bug导致ppt每次预览会执行两次转换(请求两次onlinePreview接口),在大文件尤其耗时(双倍时…
* 1. 【加密office预览】优化受密码保护的office文件检查逻辑,提升旧文件格式的兼容性
* 1. 【加密office预览】优化office文件是否受密码保护判断逻辑,避免兼容性误判
* 1. 【加密office预览】优化重新输入密码提示。
* 1. 【加密office预览】优化当密码输入错误后,不是抛出异常,而是提示用户重新输入
* 1. 优化prompt提示框的输入密码提示样式
* 1. 实现基于userToken缓存加密文件,没有userToken的加密文件不缓存
* 1. 优化docker构建方案,使用分层构建方式,采用层级缓存解决构建慢发布慢等问题。从原本5分钟左右缩短至几秒
* 1. 加密文件暂时不缓存(后续基于用户token实现,基于用户缓存)
* 1. 优化office文件下载逻辑,跳过重复下载(大量节约带宽与磁盘空间)。
* 1. 修复预览不同类型的加密office文件bug
* 实现预览加密的(受密码保护)office文件
This commit is contained in:
yl-yue
2022-07-21 03:19:46 +00:00
committed by 陈精华
parent 04703aa03c
commit acffcbfe98
26 changed files with 746 additions and 492 deletions

View File

@ -6,8 +6,8 @@ import org.springframework.stereotype.Component;
import java.io.File;
import java.util.Arrays;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @author: chenjh

View File

@ -1,6 +1,7 @@
package cn.keking.model;
import cn.keking.config.ConfigConstants;
import org.artofsolving.jodconverter.model.FileProperties;
/**
* Created by kl on 2018/1/17.
@ -13,6 +14,8 @@ public class FileAttribute {
private String name;
private String url;
private String fileKey;
private String filePassword;
private String userToken;
private String officePreviewType = ConfigConstants.getOfficePreviewType();
private String tifPreviewType;
private Boolean skipDownLoad = false;
@ -35,6 +38,12 @@ public class FileAttribute {
this.officePreviewType = officePreviewType;
}
public FileProperties toFileProperties() {
FileProperties fileProperties = new FileProperties();
fileProperties.setFilePassword(filePassword);
return fileProperties;
}
public String getFileKey() {
return fileKey;
}
@ -43,6 +52,22 @@ public class FileAttribute {
this.fileKey = fileKey;
}
public String getFilePassword() {
return filePassword;
}
public void setFilePassword(String filePassword) {
this.filePassword = filePassword;
}
public String getUserToken() {
return userToken;
}
public void setUserToken(String userToken) {
this.userToken = userToken;
}
public String getOfficePreviewType() {
return officePreviewType;
}
@ -82,6 +107,7 @@ public class FileAttribute {
public void setUrl(String url) {
this.url = url;
}
public Boolean getSkipDownLoad() {
return skipDownLoad;
}
@ -89,6 +115,7 @@ public class FileAttribute {
public void setSkipDownLoad(Boolean skipDownLoad) {
this.skipDownLoad = skipDownLoad;
}
public String getTifPreviewType() {
return tifPreviewType;
}
@ -96,4 +123,5 @@ public class FileAttribute {
public void setTifPreviewType(String previewType) {
this.tifPreviewType = previewType;
}
}

View File

@ -292,7 +292,18 @@ public class FileHandlerService {
if (StringUtils.hasText(tifPreviewType)) {
attribute.setTifPreviewType(tifPreviewType);
}
String filePassword = req.getParameter("filePassword");
if (StringUtils.hasText(filePassword)) {
attribute.setFilePassword(filePassword);
}
String userToken = req.getParameter("userToken");
if (StringUtils.hasText(userToken)) {
attribute.setUserToken(userToken);
}
}
return attribute;
}

View File

@ -1,5 +1,6 @@
package cn.keking.service;
import cn.keking.model.FileAttribute;
import org.artofsolving.jodconverter.OfficeDocumentConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -20,22 +21,23 @@ public class OfficeToPdfService {
this.officePluginManager = officePluginManager;
}
public void openOfficeToPDF(String inputFilePath, String outputFilePath) {
office2pdf(inputFilePath, outputFilePath);
public void openOfficeToPDF(String inputFilePath, String outputFilePath, FileAttribute fileAttribute) {
office2pdf(inputFilePath, outputFilePath, fileAttribute);
}
public static void converterFile(File inputFile, String outputFilePath_end, OfficeDocumentConverter converter) {
public static void converterFile(File inputFile, String outputFilePath_end, OfficeDocumentConverter converter, FileAttribute fileAttribute) {
File outputFile = new File(outputFilePath_end);
// 假如目标路径不存在,则新建该路径
if (!outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) {
logger.error("创建目录【{}】失败,请检查目录权限!",outputFilePath_end);
}
converter.convert(inputFile, outputFile);
converter.convert(inputFile, outputFile, fileAttribute.toFileProperties());
}
public void office2pdf(String inputFilePath, String outputFilePath) {
public void office2pdf(String inputFilePath, String outputFilePath, FileAttribute fileAttribute) {
OfficeDocumentConverter converter = officePluginManager.getDocumentConverter();
if (null != inputFilePath) {
File inputFile = new File(inputFilePath);
@ -45,12 +47,12 @@ public class OfficeToPdfService {
String outputFilePath_end = getOutputFilePath(inputFilePath);
if (inputFile.exists()) {
// 找不到源文件, 则返回
converterFile(inputFile, outputFilePath_end,converter);
converterFile(inputFile, outputFilePath_end, converter, fileAttribute);
}
} else {
if (inputFile.exists()) {
// 找不到源文件, 则返回
converterFile(inputFile, outputFilePath, converter);
converterFile(inputFile, outputFilePath, converter, fileAttribute);
}
}
}

View File

@ -3,11 +3,13 @@ package cn.keking.service.impl;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse;
import cn.keking.service.FilePreview;
import cn.keking.utils.DownloadUtils;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.service.OfficeToPdfService;
import cn.keking.utils.DownloadUtils;
import cn.keking.utils.OfficeUtils;
import cn.keking.web.filter.BaseUrlFilter;
import org.artofsolving.jodconverter.office.OfficeException;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
@ -42,33 +44,83 @@ public class OfficeFilePreviewImpl implements FilePreview {
String baseUrl = BaseUrlFilter.getBaseUrl();
String suffix = fileAttribute.getSuffix();
String fileName = fileAttribute.getName();
String filePassword = fileAttribute.getFilePassword();
String userToken = fileAttribute.getUserToken();
boolean isHtml = suffix.equalsIgnoreCase("xls") || suffix.equalsIgnoreCase("xlsx");
String pdfName = fileName.substring(0, fileName.lastIndexOf(".") + 1) + (isHtml ? "html" : "pdf");
String outFilePath = FILE_DIR + pdfName;
// 判断之前是否已转换过,如果转换过,直接返回,否则执行转换
if (!fileHandlerService.listConvertedFiles().containsKey(pdfName) || !ConfigConstants.isCacheEnabled()) {
String filePath;
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, null);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
String cacheFileName = userToken == null ? pdfName : userToken + "_" + pdfName;
String outFilePath = FILE_DIR + cacheFileName;
// 下载远程文件到本地,如果文件在本地已存在不会重复下载
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
}
String filePath = response.getContent();
/*
* 1. 缓存判断-如果文件已经进行转换过,就直接返回,否则执行转换
* 2. 缓存判断-加密文件基于userToken进行缓存如果没有就不缓存
*/
boolean isCached = false;
boolean isUseCached = false;
boolean isPwdProtectedOffice = false;
if (ConfigConstants.isCacheEnabled()) {
// 全局开启缓存
isUseCached = true;
if (fileHandlerService.listConvertedFiles().containsKey(cacheFileName)) {
// 存在缓存
isCached = true;
}
filePath = response.getContent();
if (StringUtils.hasText(outFilePath)) {
officeToPdfService.openOfficeToPDF(filePath, outFilePath);
if (isHtml) {
// 对转换后的文件进行操作(改变编码方式)
fileHandlerService.doActionConvertedFile(outFilePath);
if (OfficeUtils.isPwdProtected(filePath)) {
isPwdProtectedOffice = true;
if (!StringUtils.hasLength(userToken)) {
// 不缓存没有userToken的加密文件
isUseCached = false;
}
if (ConfigConstants.isCacheEnabled()) {
// 加入缓存
fileHandlerService.addConvertedFile(pdfName, fileHandlerService.getRelativePath(outFilePath));
}
} else {
isPwdProtectedOffice = OfficeUtils.isPwdProtected(filePath);
}
if (isCached == false) {
// 没有缓存执行转换逻辑
if (isPwdProtectedOffice && !StringUtils.hasLength(filePassword)) {
// 加密文件需要密码
model.addAttribute("needFilePassword", true);
return EXEL_FILE_PREVIEW_PAGE;
} else {
if (StringUtils.hasText(outFilePath)) {
try {
officeToPdfService.openOfficeToPDF(filePath, outFilePath, fileAttribute);
} catch (OfficeException e) {
if (isPwdProtectedOffice && OfficeUtils.isCompatible(filePath, filePassword) == false) {
// 加密文件密码错误,提示重新输入
model.addAttribute("needFilePassword", true);
model.addAttribute("filePasswordError", true);
return EXEL_FILE_PREVIEW_PAGE;
}
return otherFilePreview.notSupportedFile(model, fileAttribute, "抱歉,该文件版本不兼容,文件版本错误。");
}
if (isHtml) {
// 对转换后的文件进行操作(改变编码方式)
fileHandlerService.doActionConvertedFile(outFilePath);
}
if (isUseCached) {
// 加入缓存
fileHandlerService.addConvertedFile(cacheFileName, fileHandlerService.getRelativePath(outFilePath));
}
}
}
}
if (!isHtml && baseUrl != null && (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType))) {
return getPreviewType(model, fileAttribute, officePreviewType, baseUrl, pdfName, outFilePath, fileHandlerService, OFFICE_PREVIEW_TYPE_IMAGE, otherFilePreview);
return getPreviewType(model, fileAttribute, officePreviewType, baseUrl, cacheFileName, outFilePath, fileHandlerService, OFFICE_PREVIEW_TYPE_IMAGE, otherFilePreview);
}
model.addAttribute("pdfUrl", pdfName);
model.addAttribute("pdfUrl", cacheFileName);
return isHtml ? EXEL_FILE_PREVIEW_PAGE : PDF_FILE_PREVIEW_PAGE;
}
@ -88,4 +140,5 @@ public class OfficeFilePreviewImpl implements FilePreview {
return PICTURE_FILE_PREVIEW_PAGE;
}
}
}

View File

@ -87,6 +87,13 @@ public class DownloadUtils {
if (!dirFile.exists() && !dirFile.mkdirs()) {
logger.error("创建目录【{}】失败,可能是权限不够,请检查", fileDir);
}
// 文件已在本地存在,跳过文件下载
File realFile = new File(realPath);
if (realFile.exists()) {
fileAttribute.setSkipDownLoad(true);
}
return realPath;
}

View File

@ -0,0 +1,62 @@
package cn.keking.utils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.extractor.ExtractorFactory;
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
import org.springframework.lang.Nullable;
import java.io.FileInputStream;
/**
* Office工具类
*
* @author ylyue
* @since 2022/7/5
*/
public class OfficeUtils {
/**
* 判断officeword,excel,ppt文件是否受密码保护
*
* @param path office文件路径
* @return 是否受密码保护
*/
public static boolean isPwdProtected(String path) {
try {
ExtractorFactory.createExtractor(new FileInputStream(path));
} catch (EncryptedDocumentException e) {
return true;
} catch (Exception e) {
Throwable[] throwables = ExceptionUtils.getThrowables(e);
for (Throwable throwable : throwables) {
if (throwable instanceof EncryptedDocumentException) {
return true;
}
}
}
return false;
}
/**
* 判断office文件是否可打开兼容
*
* @param path office文件路径
* @param password 文件密码
* @return 是否可打开(兼容)
*/
public static synchronized boolean isCompatible(String path, @Nullable String password) {
try {
Biff8EncryptionKey.setCurrentUserPassword(password);
ExtractorFactory.createExtractor(new FileInputStream(path));
} catch (Exception e) {
return false;
} finally {
Biff8EncryptionKey.setCurrentUserPassword(null);
}
return true;
}
}

View File

@ -19,6 +19,7 @@ import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.util.HtmlUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -73,10 +74,13 @@ public class OnlinePreviewController {
String fileUrls;
try {
fileUrls = new String(Base64.decodeBase64(urls));
// 防止XSS攻击
fileUrls = HtmlUtils.htmlEscape(fileUrls);
} catch (Exception ex) {
String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, "urls");
return otherFilePreview.notSupportedFile(model, errorMsg);
}
logger.info("预览文件url{}urls{}", fileUrls, urls);
// 抽取文件并返回文件列表
String[] images = fileUrls.split("\\|");
@ -102,6 +106,11 @@ public class OnlinePreviewController {
*/
@RequestMapping(value = "/getCorsFile", method = RequestMethod.GET)
public void getCorsFile(String urlPath, HttpServletResponse response) {
if (urlPath == null || urlPath.toLowerCase().startsWith("file:") || urlPath.toLowerCase().startsWith("file%3") || !urlPath.toLowerCase().startsWith("http")) {
logger.info("读取跨域文件异常可能存在非法访问urlPath{}", urlPath);
return;
}
logger.info("下载跨域pdf文件url{}", urlPath);
try {
URL url = WebUtils.normalizedURL(urlPath);
@ -125,6 +134,4 @@ public class OnlinePreviewController {
return "success";
}
}