From 6746325bf2d84f26a6b3a36299fc8348cfd7dbbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E7=B2=BE=E5=8D=8E?= <842761733@qq.com> Date: Mon, 14 Apr 2025 16:10:29 +0800 Subject: [PATCH 1/9] update Github workflow --- .github/workflows/maven.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index c7c179272..db909c6bd 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -21,4 +21,4 @@ jobs: distribution: 'adopt' cache: maven - name: Build with Maven - run: mvn -B package --file pom.xml + run: mvn -B package -Dmaven.test.skip=true --file pom.xml From fdb40680d3bb7f674fa74e0365e2271954554048 Mon Sep 17 00:00:00 2001 From: kl <632104866@QQ.com> Date: Mon, 30 Jun 2025 17:57:45 +0800 Subject: [PATCH 2/9] =?UTF-8?q?=E5=AE=89=E5=85=A8=EF=BC=9A=E7=A6=81?= =?UTF-8?q?=E7=94=A8=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=20(#656)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 从配置文件中移除file.upload.disable配置项 - 删除FileController中的文件上传相关代码 - 移除/fileUpload POST接口 - 删除文件上传校验逻辑 - 增强系统安全性,防止恶意文件上传 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude --- server/src/main/config/application.properties | 2 - .../keking/web/controller/FileController.java | 140 ------------------ 2 files changed, 142 deletions(-) diff --git a/server/src/main/config/application.properties b/server/src/main/config/application.properties index 251f5d691..10a63eb49 100644 --- a/server/src/main/config/application.properties +++ b/server/src/main/config/application.properties @@ -161,8 +161,6 @@ watermark.angle = ${WATERMARK_ANGLE:10} #首页功能设置 -#是否禁用首页文件上传 -file.upload.disable = ${KK_FILE_UPLOAD_DISABLE:false} # 备案信息,默认为空 beian = ${KK_BEIAN:default} #禁止上传类型 diff --git a/server/src/main/java/cn/keking/web/controller/FileController.java b/server/src/main/java/cn/keking/web/controller/FileController.java index 0ea6a9140..6743b555b 100644 --- a/server/src/main/java/cn/keking/web/controller/FileController.java +++ b/server/src/main/java/cn/keking/web/controller/FileController.java @@ -53,77 +53,6 @@ public class FileController { private final String demoPath = demoDir + File.separator; public static final String BASE64_DECODE_ERROR_MSG = "Base64解码失败,请检查你的 %s 是否采用 Base64 + urlEncode 双重编码了!"; - @PostMapping("/fileUpload") - public ReturnResponse fileUpload(@RequestParam("file") MultipartFile file) { - ReturnResponse checkResult = this.fileUploadCheck(file); - if (checkResult.isFailure()) { - return checkResult; - } - File outFile = new File(fileDir + demoPath); - if (!outFile.exists() && !outFile.mkdirs()) { - logger.error("创建文件夹【{}】失败,请检查目录权限!", fileDir + demoPath); - } - String fileName = checkResult.getContent().toString(); - logger.info("上传文件:{}{}{}", fileDir, demoPath, fileName); - try (InputStream in = file.getInputStream(); OutputStream out = Files.newOutputStream(Paths.get(fileDir + demoPath + fileName))) { - StreamUtils.copy(in, out); - return ReturnResponse.success(null); - } catch (IOException e) { - logger.error("文件上传失败", e); - return ReturnResponse.failure(); - } - } - - @GetMapping("/deleteFile") - public ReturnResponse deleteFile(HttpServletRequest request, String fileName, String password) { - ReturnResponse checkResult = this.deleteFileCheck(request, fileName, password); - if (checkResult.isFailure()) { - return checkResult; - } - fileName = checkResult.getContent().toString(); - File file = new File(fileDir + demoPath + fileName); - logger.info("删除文件:{}", file.getAbsolutePath()); - if (file.exists() && !file.delete()) { - String msg = String.format("删除文件【%s】失败,请检查目录权限!", file.getPath()); - logger.error(msg); - return ReturnResponse.failure(msg); - } - WebUtils.removeSessionAttr(request, CAPTCHA_CODE); //删除缓存验证码 - return ReturnResponse.success(); - } - - /** - * 验证码方法 - */ - @RequestMapping("/deleteFile/captcha") - public void captcha(HttpServletRequest request, HttpServletResponse response) throws Exception { - if (!ConfigConstants.getDeleteCaptcha()) { - return; - } - - response.setContentType("image/jpeg"); - response.setHeader("Pragma", "no-cache"); - response.setHeader("Cache-Control", "no-cache"); - response.setDateHeader("Expires", -1); - String captchaCode = WebUtils.getSessionAttr(request, CAPTCHA_CODE); - long captchaGenerateTime = WebUtils.getLongSessionAttr(request, CAPTCHA_GENERATE_TIME); - long timeDifference = DateUtils.calculateCurrentTimeDifference(captchaGenerateTime); - - // 验证码为空,且生成验证码超过50秒,重新生成验证码 - if (timeDifference > 50 && ObjectUtils.isEmpty(captchaCode)) { - captchaCode = CaptchaUtil.generateCaptchaCode(); - // 更新验证码 - WebUtils.setSessionAttr(request, CAPTCHA_CODE, captchaCode); - WebUtils.setSessionAttr(request, CAPTCHA_GENERATE_TIME, DateUtils.getCurrentSecond()); - } else { - captchaCode = ObjectUtils.isEmpty(captchaCode) ? "wait" : captchaCode; - } - - ServletOutputStream outputStream = response.getOutputStream(); - ImageIO.write(CaptchaUtil.generateCaptchaPic(captchaCode), "jpeg", outputStream); - outputStream.close(); - } - @GetMapping("/listFiles") public List> getFiles() { List> list = new ArrayList<>(); @@ -140,70 +69,6 @@ public List> getFiles() { return list; } - /** - * 上传文件前校验 - * - * @param file 文件 - * @return 校验结果 - */ - private ReturnResponse fileUploadCheck(MultipartFile file) { - if (ConfigConstants.getFileUploadDisable()) { - return ReturnResponse.failure("文件传接口已禁用"); - } - String fileName = WebUtils.getFileNameFromMultipartFile(file); - if (fileName.lastIndexOf(".") == -1) { - return ReturnResponse.failure("不允许上传的类型"); - } - if (!KkFileUtils.isAllowedUpload(fileName)) { - return ReturnResponse.failure("不允许上传的文件类型: " + fileName); - } - if (KkFileUtils.isIllegalFileName(fileName)) { - return ReturnResponse.failure("不允许上传的文件名: " + fileName); - } - // 判断是否存在同名文件 - if (existsFile(fileName)) { - return ReturnResponse.failure("存在同名文件,请先删除原有文件再次上传"); - } - return ReturnResponse.success(fileName); - } - - - /** - * 删除文件前校验 - * - * @param fileName 文件名 - * @return 校验结果 - */ - private ReturnResponse deleteFileCheck(HttpServletRequest request, String fileName, String password) { - if (ObjectUtils.isEmpty(fileName)) { - return ReturnResponse.failure("文件名为空,删除失败!"); - } - try { - fileName = WebUtils.decodeUrl(fileName); - } catch (Exception ex) { - String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, fileName); - return ReturnResponse.failure(errorMsg + "删除失败!"); - } - assert fileName != null; - if (fileName.contains("/")) { - fileName = fileName.substring(fileName.lastIndexOf("/") + 1); - } - if (KkFileUtils.isIllegalFileName(fileName)) { - return ReturnResponse.failure("非法文件名,删除失败!"); - } - if (ObjectUtils.isEmpty(password)) { - return ReturnResponse.failure("密码 or 验证码为空,删除失败!"); - } - - String expectedPassword = ConfigConstants.getDeleteCaptcha() ? WebUtils.getSessionAttr(request, CAPTCHA_CODE) : ConfigConstants.getPassword(); - - if (!password.equalsIgnoreCase(expectedPassword)) { - logger.error("删除文件【{}】失败,密码错误!", fileName); - return ReturnResponse.failure("删除文件失败,密码错误!"); - } - return ReturnResponse.success(fileName); - } - @GetMapping("/directory") public Object directory(String urls) { String fileUrl; @@ -219,9 +84,4 @@ public Object directory(String urls) { } return RarUtils.getTree(fileUrl); } - - private boolean existsFile(String fileName) { - File file = new File(fileDir + demoPath + fileName); - return file.exists(); - } } From 6cdbf92fb057e9ada0a406c80d15c1d77563af61 Mon Sep 17 00:00:00 2001 From: kl <632104866@QQ.com> Date: Sat, 11 Oct 2025 10:41:34 +0800 Subject: [PATCH 3/9] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E7=A6=81=E7=94=A8?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E7=9A=84=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=20(#684)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/main/config/application.properties | 3 +- .../cn/keking/config/ConfigConstants.java | 2 +- .../keking/web/controller/FileController.java | 140 ++++++++++++++++++ server/src/main/resources/web/main/index.ftl | 12 ++ 4 files changed, 155 insertions(+), 2 deletions(-) diff --git a/server/src/main/config/application.properties b/server/src/main/config/application.properties index 10a63eb49..0099bc544 100644 --- a/server/src/main/config/application.properties +++ b/server/src/main/config/application.properties @@ -159,8 +159,9 @@ watermark.height = ${WATERMARK_HEIGHT:80} #水印倾斜度数,要求设置在大于等于0,小于90 watermark.angle = ${WATERMARK_ANGLE:10} - #首页功能设置 +#是否禁用首页文件上传 +file.upload.disable = ${KK_FILE_UPLOAD_DISABLE:true} # 备案信息,默认为空 beian = ${KK_BEIAN:default} #禁止上传类型 diff --git a/server/src/main/java/cn/keking/config/ConfigConstants.java b/server/src/main/java/cn/keking/config/ConfigConstants.java index 432ea2dc2..9310441c7 100644 --- a/server/src/main/java/cn/keking/config/ConfigConstants.java +++ b/server/src/main/java/cn/keking/config/ConfigConstants.java @@ -426,7 +426,7 @@ public static Boolean getFileUploadDisable() { return fileUploadDisable; } - @Value("${file.upload.disable:false}") + @Value("${file.upload.disable:true}") public void setFileUploadDisable(Boolean fileUploadDisable) { setFileUploadDisableValue(fileUploadDisable); } diff --git a/server/src/main/java/cn/keking/web/controller/FileController.java b/server/src/main/java/cn/keking/web/controller/FileController.java index 6743b555b..0ea6a9140 100644 --- a/server/src/main/java/cn/keking/web/controller/FileController.java +++ b/server/src/main/java/cn/keking/web/controller/FileController.java @@ -53,6 +53,77 @@ public class FileController { private final String demoPath = demoDir + File.separator; public static final String BASE64_DECODE_ERROR_MSG = "Base64解码失败,请检查你的 %s 是否采用 Base64 + urlEncode 双重编码了!"; + @PostMapping("/fileUpload") + public ReturnResponse fileUpload(@RequestParam("file") MultipartFile file) { + ReturnResponse checkResult = this.fileUploadCheck(file); + if (checkResult.isFailure()) { + return checkResult; + } + File outFile = new File(fileDir + demoPath); + if (!outFile.exists() && !outFile.mkdirs()) { + logger.error("创建文件夹【{}】失败,请检查目录权限!", fileDir + demoPath); + } + String fileName = checkResult.getContent().toString(); + logger.info("上传文件:{}{}{}", fileDir, demoPath, fileName); + try (InputStream in = file.getInputStream(); OutputStream out = Files.newOutputStream(Paths.get(fileDir + demoPath + fileName))) { + StreamUtils.copy(in, out); + return ReturnResponse.success(null); + } catch (IOException e) { + logger.error("文件上传失败", e); + return ReturnResponse.failure(); + } + } + + @GetMapping("/deleteFile") + public ReturnResponse deleteFile(HttpServletRequest request, String fileName, String password) { + ReturnResponse checkResult = this.deleteFileCheck(request, fileName, password); + if (checkResult.isFailure()) { + return checkResult; + } + fileName = checkResult.getContent().toString(); + File file = new File(fileDir + demoPath + fileName); + logger.info("删除文件:{}", file.getAbsolutePath()); + if (file.exists() && !file.delete()) { + String msg = String.format("删除文件【%s】失败,请检查目录权限!", file.getPath()); + logger.error(msg); + return ReturnResponse.failure(msg); + } + WebUtils.removeSessionAttr(request, CAPTCHA_CODE); //删除缓存验证码 + return ReturnResponse.success(); + } + + /** + * 验证码方法 + */ + @RequestMapping("/deleteFile/captcha") + public void captcha(HttpServletRequest request, HttpServletResponse response) throws Exception { + if (!ConfigConstants.getDeleteCaptcha()) { + return; + } + + response.setContentType("image/jpeg"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Cache-Control", "no-cache"); + response.setDateHeader("Expires", -1); + String captchaCode = WebUtils.getSessionAttr(request, CAPTCHA_CODE); + long captchaGenerateTime = WebUtils.getLongSessionAttr(request, CAPTCHA_GENERATE_TIME); + long timeDifference = DateUtils.calculateCurrentTimeDifference(captchaGenerateTime); + + // 验证码为空,且生成验证码超过50秒,重新生成验证码 + if (timeDifference > 50 && ObjectUtils.isEmpty(captchaCode)) { + captchaCode = CaptchaUtil.generateCaptchaCode(); + // 更新验证码 + WebUtils.setSessionAttr(request, CAPTCHA_CODE, captchaCode); + WebUtils.setSessionAttr(request, CAPTCHA_GENERATE_TIME, DateUtils.getCurrentSecond()); + } else { + captchaCode = ObjectUtils.isEmpty(captchaCode) ? "wait" : captchaCode; + } + + ServletOutputStream outputStream = response.getOutputStream(); + ImageIO.write(CaptchaUtil.generateCaptchaPic(captchaCode), "jpeg", outputStream); + outputStream.close(); + } + @GetMapping("/listFiles") public List> getFiles() { List> list = new ArrayList<>(); @@ -69,6 +140,70 @@ public List> getFiles() { return list; } + /** + * 上传文件前校验 + * + * @param file 文件 + * @return 校验结果 + */ + private ReturnResponse fileUploadCheck(MultipartFile file) { + if (ConfigConstants.getFileUploadDisable()) { + return ReturnResponse.failure("文件传接口已禁用"); + } + String fileName = WebUtils.getFileNameFromMultipartFile(file); + if (fileName.lastIndexOf(".") == -1) { + return ReturnResponse.failure("不允许上传的类型"); + } + if (!KkFileUtils.isAllowedUpload(fileName)) { + return ReturnResponse.failure("不允许上传的文件类型: " + fileName); + } + if (KkFileUtils.isIllegalFileName(fileName)) { + return ReturnResponse.failure("不允许上传的文件名: " + fileName); + } + // 判断是否存在同名文件 + if (existsFile(fileName)) { + return ReturnResponse.failure("存在同名文件,请先删除原有文件再次上传"); + } + return ReturnResponse.success(fileName); + } + + + /** + * 删除文件前校验 + * + * @param fileName 文件名 + * @return 校验结果 + */ + private ReturnResponse deleteFileCheck(HttpServletRequest request, String fileName, String password) { + if (ObjectUtils.isEmpty(fileName)) { + return ReturnResponse.failure("文件名为空,删除失败!"); + } + try { + fileName = WebUtils.decodeUrl(fileName); + } catch (Exception ex) { + String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, fileName); + return ReturnResponse.failure(errorMsg + "删除失败!"); + } + assert fileName != null; + if (fileName.contains("/")) { + fileName = fileName.substring(fileName.lastIndexOf("/") + 1); + } + if (KkFileUtils.isIllegalFileName(fileName)) { + return ReturnResponse.failure("非法文件名,删除失败!"); + } + if (ObjectUtils.isEmpty(password)) { + return ReturnResponse.failure("密码 or 验证码为空,删除失败!"); + } + + String expectedPassword = ConfigConstants.getDeleteCaptcha() ? WebUtils.getSessionAttr(request, CAPTCHA_CODE) : ConfigConstants.getPassword(); + + if (!password.equalsIgnoreCase(expectedPassword)) { + logger.error("删除文件【{}】失败,密码错误!", fileName); + return ReturnResponse.failure("删除文件失败,密码错误!"); + } + return ReturnResponse.success(fileName); + } + @GetMapping("/directory") public Object directory(String urls) { String fileUrl; @@ -84,4 +219,9 @@ public Object directory(String urls) { } return RarUtils.getTree(fileUrl); } + + private boolean existsFile(String fileName) { + File file = new File(fileDir + demoPath + fileName); + return file.exists(); + } } diff --git a/server/src/main/resources/web/main/index.ftl b/server/src/main/resources/web/main/index.ftl index c4ebb5f7d..76d232e32 100644 --- a/server/src/main/resources/web/main/index.ftl +++ b/server/src/main/resources/web/main/index.ftl @@ -148,6 +148,18 @@ + <#else> +
+

+ 文件上传功能默认已禁用。如需开启,请通过以下方式配置: +
+ • 配置文件:file.upload.disable=false +
+ • 环境变量:KK_FILE_UPLOAD_DISABLE=false +
+ 请注意:文件上传限开发环境调试使用,生产环境建议保持关闭状态,避免非法上传导致的安全隐患。 +

+
From a9787b0add6977dc7c115a45a47d9b797594f448 Mon Sep 17 00:00:00 2001 From: kl <632104866@QQ.com> Date: Sat, 11 Oct 2025 11:52:49 +0800 Subject: [PATCH 4/9] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9AJSON=20=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=A0=BC=E5=BC=8F=E5=8C=96=E9=A2=84=E8=A7=88=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=20(#685)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/cn/keking/model/FileType.java | 7 +- .../java/cn/keking/service/FilePreview.java | 1 + .../service/impl/JsonFilePreviewImpl.java | 27 +++ server/src/main/resources/web/json.ftl | 214 ++++++++++++++++++ 4 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 server/src/main/java/cn/keking/service/impl/JsonFilePreviewImpl.java create mode 100644 server/src/main/resources/web/json.ftl diff --git a/server/src/main/java/cn/keking/model/FileType.java b/server/src/main/java/cn/keking/model/FileType.java index 22d69979d..dbe054b75 100644 --- a/server/src/main/java/cn/keking/model/FileType.java +++ b/server/src/main/java/cn/keking/model/FileType.java @@ -22,6 +22,7 @@ public enum FileType { MEDIACONVERT("mediaFilePreviewImpl"), MARKDOWN("markdownFilePreviewImpl"), XML("xmlFilePreviewImpl"), + JSON("jsonFilePreviewImpl"), CAD("cadFilePreviewImpl"), TIFF("tiffFilePreviewImpl"), OFD("ofdFilePreviewImpl"), @@ -44,12 +45,13 @@ public enum FileType { private static final String[] DCM_TYPES = {"dcm"}; private static final String[] DRAWIO_TYPES = {"drawio"}; private static final String[] XML_TYPES = {"xml","xbrl"}; + private static final String[] JSON_TYPES = {"json"}; private static final String[] TIFF_TYPES = {"tif", "tiff"}; private static final String[] OFD_TYPES = {"ofd"}; private static final String[] SVG_TYPES = {"svg"}; private static final String[] CAD_TYPES = {"dwg", "dxf", "dwf", "iges", "igs", "dwt", "dng", "ifc", "dwfx", "stl", "cf2", "plt"}; private static final String[] SSIM_TEXT_TYPES = ConfigConstants.getSimText(); - private static final String[] CODES = {"java", "c", "php", "go", "python", "py", "js", "html", "ftl", "css", "lua", "sh", "rb", "yaml", "yml", "json", "h", "cpp", "cs", "aspx", "jsp", "sql"}; + private static final String[] CODES = {"java", "c", "php", "go", "python", "py", "js", "html", "ftl", "css", "lua", "sh", "rb", "yaml", "yml", "h", "cpp", "cs", "aspx", "jsp", "sql"}; private static final String[] MEDIA_TYPES = ConfigConstants.getMedia(); public static final String[] MEDIA_CONVERT_TYPES = ConfigConstants.getConvertMedias(); private static final Map FILE_TYPE_MAPPER = new HashMap<>(); @@ -109,6 +111,9 @@ public enum FileType { for (String xml : XML_TYPES) { FILE_TYPE_MAPPER.put(xml, FileType.XML); } + for (String json : JSON_TYPES) { + FILE_TYPE_MAPPER.put(json, FileType.JSON); + } FILE_TYPE_MAPPER.put("md", FileType.MARKDOWN); FILE_TYPE_MAPPER.put("pdf", FileType.PDF); FILE_TYPE_MAPPER.put("bpmn", FileType.BPMN); diff --git a/server/src/main/java/cn/keking/service/FilePreview.java b/server/src/main/java/cn/keking/service/FilePreview.java index 388b251fa..5a018b4d5 100644 --- a/server/src/main/java/cn/keking/service/FilePreview.java +++ b/server/src/main/java/cn/keking/service/FilePreview.java @@ -26,6 +26,7 @@ public interface FilePreview { String CODE_FILE_PREVIEW_PAGE = "code"; String EXEL_FILE_PREVIEW_PAGE = "html"; String XML_FILE_PREVIEW_PAGE = "xml"; + String JSON_FILE_PREVIEW_PAGE = "json"; String MARKDOWN_FILE_PREVIEW_PAGE = "markdown"; String BPMN_FILE_PREVIEW_PAGE = "bpmn"; String DCM_FILE_PREVIEW_PAGE = "dcm"; diff --git a/server/src/main/java/cn/keking/service/impl/JsonFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/JsonFilePreviewImpl.java new file mode 100644 index 000000000..57a653fd4 --- /dev/null +++ b/server/src/main/java/cn/keking/service/impl/JsonFilePreviewImpl.java @@ -0,0 +1,27 @@ +package cn.keking.service.impl; + +import cn.keking.model.FileAttribute; +import cn.keking.service.FilePreview; +import org.springframework.stereotype.Service; +import org.springframework.ui.Model; + +/** + * @author kl (http://kailing.pub) + * @since 2025/01/11 + * JSON 文件预览处理实现 + */ +@Service +public class JsonFilePreviewImpl implements FilePreview { + + private final SimTextFilePreviewImpl simTextFilePreview; + + public JsonFilePreviewImpl(SimTextFilePreviewImpl simTextFilePreview) { + this.simTextFilePreview = simTextFilePreview; + } + + @Override + public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) { + simTextFilePreview.filePreviewHandle(url, model, fileAttribute); + return JSON_FILE_PREVIEW_PAGE; + } +} diff --git a/server/src/main/resources/web/json.ftl b/server/src/main/resources/web/json.ftl new file mode 100644 index 000000000..efca0e1e2 --- /dev/null +++ b/server/src/main/resources/web/json.ftl @@ -0,0 +1,214 @@ + + + + + + JSON文件预览 + <#include "*/commonHeader.ftl"> + + + + + + + + + +
+
+
+

+ ${file.name} +

+
+ + +
+
+
+
+ +
+
+
+ + + + + From 51653483b916efe86ac6164ea8f0e1defda61a9a Mon Sep 17 00:00:00 2001 From: kl <632104866@QQ.com> Date: Sat, 11 Oct 2025 17:54:17 +0800 Subject: [PATCH 5/9] Kl json (#686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 新增:JSON 文件格式化预览功能 * 优化:优化 JSON 文件格式化预览效果 --- .../service/impl/JsonFilePreviewImpl.java | 72 ++++- .../service/impl/SimTextFilePreviewImpl.java | 4 +- server/src/main/resources/web/json.ftl | 262 +++++++++++++++--- 3 files changed, 286 insertions(+), 52 deletions(-) diff --git a/server/src/main/java/cn/keking/service/impl/JsonFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/JsonFilePreviewImpl.java index 57a653fd4..b5d15714c 100644 --- a/server/src/main/java/cn/keking/service/impl/JsonFilePreviewImpl.java +++ b/server/src/main/java/cn/keking/service/impl/JsonFilePreviewImpl.java @@ -1,9 +1,22 @@ package cn.keking.service.impl; +import cn.keking.config.ConfigConstants; import cn.keking.model.FileAttribute; +import cn.keking.model.ReturnResponse; +import cn.keking.service.FileHandlerService; import cn.keking.service.FilePreview; +import cn.keking.utils.DownloadUtils; +import cn.keking.utils.KkFileUtils; +import org.apache.commons.codec.binary.Base64; import org.springframework.stereotype.Service; import org.springframework.ui.Model; +import org.springframework.web.util.HtmlUtils; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + /** * @author kl (http://kailing.pub) @@ -13,15 +26,66 @@ @Service public class JsonFilePreviewImpl implements FilePreview { - private final SimTextFilePreviewImpl simTextFilePreview; + private final FileHandlerService fileHandlerService; + private final OtherFilePreviewImpl otherFilePreview; - public JsonFilePreviewImpl(SimTextFilePreviewImpl simTextFilePreview) { - this.simTextFilePreview = simTextFilePreview; + public JsonFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) { + this.fileHandlerService = fileHandlerService; + this.otherFilePreview = otherFilePreview; } @Override public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) { - simTextFilePreview.filePreviewHandle(url, model, fileAttribute); + String fileName = fileAttribute.getName(); + boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); + String filePath = fileAttribute.getOriginFilePath(); + + if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(fileName) || !ConfigConstants.isCacheEnabled()) { + ReturnResponse response = DownloadUtils.downLoad(fileAttribute, fileName); + if (response.isFailure()) { + return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); + } + filePath = response.getContent(); + if (ConfigConstants.isCacheEnabled()) { + fileHandlerService.addConvertedFile(fileName, filePath); + } + try { + String fileData = readJsonFile(filePath, fileName); + String escapedData = HtmlUtils.htmlEscape(fileData); + String base64Data = Base64.encodeBase64String(escapedData.getBytes(StandardCharsets.UTF_8)); + model.addAttribute("textData", base64Data); + } catch (IOException e) { + return otherFilePreview.notSupportedFile(model, fileAttribute, e.getLocalizedMessage()); + } + return JSON_FILE_PREVIEW_PAGE; + } + + String fileData = null; + try { + fileData = HtmlUtils.htmlEscape(readJsonFile(filePath, fileName)); + } catch (IOException e) { + e.printStackTrace(); + } + String base64Data = Base64.encodeBase64String(fileData.getBytes(StandardCharsets.UTF_8)); + model.addAttribute("textData", base64Data); return JSON_FILE_PREVIEW_PAGE; } + + /** + * 读取 JSON 文件,强制使用 UTF-8 编码 + * JSON 标准规定必须使用 UTF-8 编码 + */ + private String readJsonFile(String filePath, String fileName) throws IOException { + File file = new File(filePath); + if (KkFileUtils.isIllegalFileName(fileName)) { + return null; + } + if (!file.exists() || file.length() == 0) { + return ""; + } + + // JSON 标准规定使用 UTF-8 编码,不依赖自动检测 + byte[] bytes = Files.readAllBytes(Paths.get(filePath)); + return new String(bytes, StandardCharsets.UTF_8); + } } diff --git a/server/src/main/java/cn/keking/service/impl/SimTextFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/SimTextFilePreviewImpl.java index 9543abb5b..ce7c6d677 100644 --- a/server/src/main/java/cn/keking/service/impl/SimTextFilePreviewImpl.java +++ b/server/src/main/java/cn/keking/service/impl/SimTextFilePreviewImpl.java @@ -47,7 +47,7 @@ public String filePreviewHandle(String url, Model model, FileAttribute fileAttri } try { String fileData = HtmlUtils.htmlEscape(textData(filePath,fileName)); - model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes())); + model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes(StandardCharsets.UTF_8))); } catch (IOException e) { return otherFilePreview.notSupportedFile(model, fileAttribute, e.getLocalizedMessage()); } @@ -59,7 +59,7 @@ public String filePreviewHandle(String url, Model model, FileAttribute fileAttri } catch (IOException e) { e.printStackTrace(); } - model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes())); + model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes(StandardCharsets.UTF_8))); return TXT_FILE_PREVIEW_PAGE; } diff --git a/server/src/main/resources/web/json.ftl b/server/src/main/resources/web/json.ftl index efca0e1e2..16a64fb63 100644 --- a/server/src/main/resources/web/json.ftl +++ b/server/src/main/resources/web/json.ftl @@ -17,25 +17,18 @@ max-width: 100%; padding: 20px; } - .panel-body { - padding: 0; - } #json { - padding: 20px; - background-color: #f8f9fa; - overflow-x: auto; - } - #text_view { - padding: 20px; - background-color: #ffffff; + padding: 0; overflow-x: auto; } pre { - margin: 0; + padding: 20px; + padding-left: 65px; /* 为行号留出空间 */ white-space: pre-wrap; word-wrap: break-word; font-size: 14px; line-height: 1.6; + position: relative; } .json-key { color: #881391; @@ -55,11 +48,31 @@ color: #808080; font-weight: bold; } - .btn-group { - margin-bottom: 10px; + .json-toggle { + cursor: pointer; + color: #666; + user-select: none; + display: inline-block; + width: 16px; + font-weight: bold; } - .view-mode-btn { - min-width: 100px; + .json-toggle:hover { + color: #333; + } + .json-node { + display: block; + } + .line-number { + position: absolute; + left: 0; + width: 55px; + color: #999; + font-size: 12px; + user-select: none; + text-align: right; + padding-right: 10px; + border-right: 1px solid #ddd; + background-color: #f8f9fa; } @@ -68,18 +81,21 @@
-
+

- ${file.name} + + ${file.name} +

-
- - -
-
-
- + +
@@ -89,22 +105,20 @@ * 初始化 */ window.onload = function () { + $("#formatted_btn").hide(); initWaterMark(); loadJsonData(); } /** * HTML 反转义(用于还原后端转义的内容) + * 使用浏览器的 DOM 来正确解码所有 HTML 实体 */ function htmlUnescape(str) { if (!str || str.length === 0) return ""; - var s = str; - s = s.replace(/"/g, '"'); - s = s.replace(/'/g, "'"); - s = s.replace(/</g, "<"); - s = s.replace(/>/g, ">"); - s = s.replace(/&/g, "&"); - return s; + var textarea = document.createElement('textarea'); + textarea.innerHTML = str; + return textarea.value; } /** @@ -131,8 +145,132 @@ return str; } + // 全局行号计数器 + var lineNumber = 1; + + /** + * 构建可展开/收起的 JSON 树形结构 + */ + function buildJsonTree(obj, indent, skipLineNumber) { + indent = indent || 0; + skipLineNumber = skipLineNumber || false; + var html = ''; + var indentStr = ' '.repeat(indent); + + if (obj === null) { + return 'null'; + } + + if (typeof obj !== 'object') { + if (typeof obj === 'string') { + // 转义特殊字符,避免换行和制表符破坏布局 + var escapedStr = obj + .replace(/\\/g, '\\\\') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\t/g, '\\t') + .replace(/"/g, '\\"'); + return '"' + htmlEscape(escapedStr) + '"'; + } else if (typeof obj === 'number') { + return '' + obj + ''; + } else if (typeof obj === 'boolean') { + return '' + obj + ''; + } + return htmlEscape(String(obj)); + } + + var isArray = Array.isArray(obj); + var entries = isArray ? obj : Object.keys(obj); + var openBracket = isArray ? '[' : '{'; + var closeBracket = isArray ? ']' : '}'; + + if (entries.length === 0) { + return openBracket + closeBracket; + } + + var nodeId = 'node_' + Math.random().toString(36).substr(2, 9); + + // 如果不跳过行号,说明是新的一行 + if (!skipLineNumber) { + html += '' + lineNumber++ + ''; + } + + html += ' '; + html += openBracket + '\n'; + html += '
'; + + for (var i = 0; i < entries.length; i++) { + var key = isArray ? i : entries[i]; + var value = isArray ? entries[i] : obj[entries[i]]; + + html += '' + lineNumber++ + ''; + html += indentStr + ' '; + if (!isArray) { + html += '"' + htmlEscape(key) + '": '; + } + + // 如果值是对象或数组,跳过它的行号(因为已经在上面添加了) + html += buildJsonTree(value, indent + 1, true); + + if (i < entries.length - 1) { + html += ','; + } + html += '\n'; + } + + html += '
'; + html += '' + lineNumber++ + ''; + html += indentStr + closeBracket; + + return html; + } + + /** + * 切换 JSON 节点展开/收起 + */ + function toggleJsonNode(nodeId) { + var node = document.getElementById(nodeId); + var toggle = event.target; + + if (node.style.display === 'none') { + node.style.display = 'block'; + toggle.textContent = '▼'; + } else { + node.style.display = 'none'; + toggle.textContent = '▶'; + } + } + + /** + * 全部展开 + */ + function expandAll() { + var nodes = document.querySelectorAll('.json-node'); + var toggles = document.querySelectorAll('.json-toggle'); + nodes.forEach(function(node) { + node.style.display = 'block'; + }); + toggles.forEach(function(toggle) { + toggle.textContent = '▼'; + }); + } + /** - * JSON 语法高亮 + * 全部收起 + */ + function collapseAll() { + var nodes = document.querySelectorAll('.json-node'); + var toggles = document.querySelectorAll('.json-toggle'); + nodes.forEach(function(node) { + node.style.display = 'none'; + }); + toggles.forEach(function(toggle) { + toggle.textContent = '▶'; + }); + } + + /** + * JSON 语法高亮(简单版本,用于原始文本视图) */ function syntaxHighlight(json) { json = json.replace(/&/g, '&').replace(//g, '>'); @@ -153,12 +291,36 @@ }); } + /** + * UTF-8 解码 Base64(正确处理中文等 UTF-8 字符) + */ + function decodeBase64UTF8(base64Str) { + try { + // 方法1:使用现代浏览器的 TextDecoder API(推荐) + if (typeof TextDecoder !== 'undefined') { + var binaryString = window.atob(base64Str); + var bytes = new Uint8Array(binaryString.length); + for (var i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return new TextDecoder('utf-8').decode(bytes); + } + + // 方法2:降级方案 + return decodeURIComponent(escape(window.atob(base64Str))); + } catch (e) { + console.error('Base64 decode error:', e); + // 最后降级到 Base64.js 库 + return Base64.decode(base64Str); + } + } + /** * 加载 JSON 数据 */ function loadJsonData() { try { - var textData = Base64.decode($("#textData").val()); + var textData = decodeBase64UTF8($("#textData").val()); // 1. 先反转义 HTML 实体(因为后端已经转义过) textData = htmlUnescape(textData); @@ -167,15 +329,27 @@ textData = removeBOM(textData); // 保存原始文本(用于显示时再次转义以保证安全) - window.rawText = "
" + htmlEscape(textData) + "
"; + window.rawText = "
" + htmlEscape(textData) + "
"; // 尝试解析并格式化 JSON try { var jsonObj = JSON.parse(textData); - var formattedJson = JSON.stringify(jsonObj, null, 4); - window.formattedJson = "
" + syntaxHighlight(formattedJson) + "
"; - // 默认显示格式化视图 + // 重置行号计数器 + lineNumber = 1; + + // 构建树形视图 + var treeHtml = '
'; + treeHtml += '
'; + treeHtml += ''; + treeHtml += ''; + treeHtml += '
'; + treeHtml += '
';
+                treeHtml += buildJsonTree(jsonObj, 0);
+                treeHtml += '
'; + window.formattedJson = treeHtml; + + // 默认显示树形视图 $("#json").html(window.formattedJson); } catch (e) { // 如果不是有效的 JSON,显示错误并回退到原始文本 @@ -193,19 +367,15 @@ */ $(function () { $("#formatted_btn").click(function () { - $("#json").show(); - $("#text_view").hide(); $("#json").html(window.formattedJson); - $("#formatted_btn").removeClass("btn-default").addClass("btn-primary"); - $("#raw_btn").removeClass("btn-primary").addClass("btn-default"); + $("#raw_btn").show(); + $("#formatted_btn").hide(); }); $("#raw_btn").click(function () { - $("#json").hide(); - $("#text_view").show(); - $("#text_view").html(window.rawText); - $("#raw_btn").removeClass("btn-default").addClass("btn-primary"); - $("#formatted_btn").removeClass("btn-primary").addClass("btn-default"); + $("#json").html(window.rawText); + $("#formatted_btn").show(); + $("#raw_btn").hide(); }); }); From f6c6e22b0d8574a5fcdd1fbfb28f2ee713c69b42 Mon Sep 17 00:00:00 2001 From: kl <632104866@QQ.com> Date: Sat, 11 Oct 2025 20:05:43 +0800 Subject: [PATCH 6/9] =?UTF-8?q?=E5=8D=87=E7=BA=A7=EF=BC=9A=20JDK1.8=20?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=E5=88=B0=20JDK21=20,spring-boot=20=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E4=BB=8E=202.4.2=20=E5=8D=87=E7=BA=A7=E5=88=B0=203.5.?= =?UTF-8?q?6=20(#687)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 6 ++--- server/pom.xml | 24 +++++++++---------- .../keking/config/ConfigRefreshComponent.java | 2 +- .../keking/service/FileConvertQueueTask.java | 2 +- .../cn/keking/service/FileHandlerService.java | 2 +- .../keking/service/OfficePluginManager.java | 4 ++-- .../cache/impl/CacheServiceJDKImpl.java | 2 +- .../java/cn/keking/utils/DownloadUtils.java | 6 ++--- .../main/java/cn/keking/utils/WebUtils.java | 10 ++++---- .../keking/web/controller/FileController.java | 6 ++--- .../controller/OnlinePreviewController.java | 10 ++++---- .../keking/web/filter/AttributeSetFilter.java | 4 ++-- .../cn/keking/web/filter/BaseUrlFilter.java | 4 ++-- .../keking/web/filter/ChinesePathFilter.java | 2 +- .../web/filter/SecurityFilterProxy.java | 8 +++---- .../cn/keking/web/filter/TrustDirFilter.java | 2 +- .../cn/keking/web/filter/TrustHostFilter.java | 12 +++++----- .../cn/keking/web/filter/UrlCheckFilter.java | 6 ++--- 18 files changed, 55 insertions(+), 57 deletions(-) diff --git a/pom.xml b/pom.xml index 90bf39ece..fdf282ea5 100644 --- a/pom.xml +++ b/pom.xml @@ -9,14 +9,14 @@ 4.4.0 - 1.8 + 21 4.4.6 - 2.4.2 + 3.5.6 5.2.2 1.0.6 1.4.20 7.5.5 - 3.2.0 + 3.22.0 16.02-2.01 1.0 2.7.7 diff --git a/server/pom.xml b/server/pom.xml index 92a6a8b75..e4601a6f1 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -44,17 +44,8 @@ org.springframework.boot spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-tomcat - - - - - org.springframework.boot - spring-boot-starter-jetty + org.springframework.boot spring-boot-starter-freemarker @@ -100,9 +91,8 @@ - org.apache.httpcomponents - httpclient - ${httpcomponents.version} + org.apache.httpcomponents.client5 + httpclient5 @@ -327,6 +317,14 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + true + + org.springframework.boot spring-boot-maven-plugin diff --git a/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java b/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java index 3482c5144..d9a11f73b 100644 --- a/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java +++ b/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java @@ -5,7 +5,7 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; +import jakarta.annotation.PostConstruct; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; diff --git a/server/src/main/java/cn/keking/service/FileConvertQueueTask.java b/server/src/main/java/cn/keking/service/FileConvertQueueTask.java index e5d624853..3da38d3cf 100644 --- a/server/src/main/java/cn/keking/service/FileConvertQueueTask.java +++ b/server/src/main/java/cn/keking/service/FileConvertQueueTask.java @@ -8,7 +8,7 @@ import org.springframework.stereotype.Service; import org.springframework.ui.ExtendedModelMap; -import javax.annotation.PostConstruct; +import jakarta.annotation.PostConstruct; import java.util.concurrent.TimeUnit; /** diff --git a/server/src/main/java/cn/keking/service/FileHandlerService.java b/server/src/main/java/cn/keking/service/FileHandlerService.java index 2444bc750..caecdccc6 100644 --- a/server/src/main/java/cn/keking/service/FileHandlerService.java +++ b/server/src/main/java/cn/keking/service/FileHandlerService.java @@ -31,7 +31,7 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import java.awt.image.BufferedImage; import java.io.*; import java.net.URLDecoder; diff --git a/server/src/main/java/cn/keking/service/OfficePluginManager.java b/server/src/main/java/cn/keking/service/OfficePluginManager.java index 1a01128f2..7e7033057 100644 --- a/server/src/main/java/cn/keking/service/OfficePluginManager.java +++ b/server/src/main/java/cn/keking/service/OfficePluginManager.java @@ -15,8 +15,8 @@ import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; diff --git a/server/src/main/java/cn/keking/service/cache/impl/CacheServiceJDKImpl.java b/server/src/main/java/cn/keking/service/cache/impl/CacheServiceJDKImpl.java index f3430caed..2f52c070f 100644 --- a/server/src/main/java/cn/keking/service/cache/impl/CacheServiceJDKImpl.java +++ b/server/src/main/java/cn/keking/service/cache/impl/CacheServiceJDKImpl.java @@ -7,7 +7,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; -import javax.annotation.PostConstruct; +import jakarta.annotation.PostConstruct; import java.util.ArrayList; import java.util.List; import java.util.Map; diff --git a/server/src/main/java/cn/keking/utils/DownloadUtils.java b/server/src/main/java/cn/keking/utils/DownloadUtils.java index 5f8914bfb..c42a53f60 100644 --- a/server/src/main/java/cn/keking/utils/DownloadUtils.java +++ b/server/src/main/java/cn/keking/utils/DownloadUtils.java @@ -6,9 +6,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.mola.galimatias.GalimatiasParseException; import org.apache.commons.io.FileUtils; -import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.DefaultRedirectStrategy; -import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.impl.DefaultRedirectStrategy; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpMethod; diff --git a/server/src/main/java/cn/keking/utils/WebUtils.java b/server/src/main/java/cn/keking/utils/WebUtils.java index de14fb642..53cedb888 100644 --- a/server/src/main/java/cn/keking/utils/WebUtils.java +++ b/server/src/main/java/cn/keking/utils/WebUtils.java @@ -1,16 +1,16 @@ package cn.keking.utils; import io.mola.galimatias.GalimatiasParseException; +import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.util.Base64Utils; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.util.HtmlUtils; -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; @@ -290,7 +290,7 @@ public static String decodeBase64String(String source, Charset charsets) { * https://github.com/kekingcn/kkFileView/pull/340 */ try { - return new String(Base64Utils.decodeFromString(source.replaceAll(" ", "+").replaceAll("\n", "")), charsets); + return new String(Base64.decodeBase64(source.replaceAll(" ", "+").replaceAll("\n", "")), charsets); } catch (Exception e) { if (e.getMessage().toLowerCase().contains(BASE64_MSG)) { LOGGER.error("url解码异常,接入方法错误未使用BASE64"); diff --git a/server/src/main/java/cn/keking/web/controller/FileController.java b/server/src/main/java/cn/keking/web/controller/FileController.java index 0ea6a9140..609d35ea6 100644 --- a/server/src/main/java/cn/keking/web/controller/FileController.java +++ b/server/src/main/java/cn/keking/web/controller/FileController.java @@ -19,9 +19,9 @@ import org.springframework.web.multipart.MultipartFile; import javax.imageio.ImageIO; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; import java.io.InputStream; diff --git a/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java b/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java index bd324d576..2161e23a7 100644 --- a/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java +++ b/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java @@ -11,9 +11,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import fr.opensagres.xdocreport.core.io.IOUtils; import org.apache.commons.codec.binary.Base64; -import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.DefaultRedirectStrategy; -import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.impl.DefaultRedirectStrategy; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpMethod; @@ -28,8 +28,8 @@ import org.springframework.web.client.RequestCallback; import org.springframework.web.client.RestTemplate; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.net.URL; diff --git a/server/src/main/java/cn/keking/web/filter/AttributeSetFilter.java b/server/src/main/java/cn/keking/web/filter/AttributeSetFilter.java index 5f2088771..1688d504f 100644 --- a/server/src/main/java/cn/keking/web/filter/AttributeSetFilter.java +++ b/server/src/main/java/cn/keking/web/filter/AttributeSetFilter.java @@ -4,8 +4,8 @@ import cn.keking.config.WatermarkConfigConstants; import cn.keking.utils.KkFileUtils; -import javax.servlet.*; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; /** diff --git a/server/src/main/java/cn/keking/web/filter/BaseUrlFilter.java b/server/src/main/java/cn/keking/web/filter/BaseUrlFilter.java index f7a0ee8e3..fdb807d2b 100644 --- a/server/src/main/java/cn/keking/web/filter/BaseUrlFilter.java +++ b/server/src/main/java/cn/keking/web/filter/BaseUrlFilter.java @@ -4,8 +4,8 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.web.context.request.RequestContextHolder; -import javax.servlet.*; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; /** diff --git a/server/src/main/java/cn/keking/web/filter/ChinesePathFilter.java b/server/src/main/java/cn/keking/web/filter/ChinesePathFilter.java index 5e126121c..c628d900c 100644 --- a/server/src/main/java/cn/keking/web/filter/ChinesePathFilter.java +++ b/server/src/main/java/cn/keking/web/filter/ChinesePathFilter.java @@ -1,7 +1,7 @@ package cn.keking.web.filter; -import javax.servlet.*; +import jakarta.servlet.*; import java.io.IOException; /** diff --git a/server/src/main/java/cn/keking/web/filter/SecurityFilterProxy.java b/server/src/main/java/cn/keking/web/filter/SecurityFilterProxy.java index a132c2b22..fc8b835b9 100644 --- a/server/src/main/java/cn/keking/web/filter/SecurityFilterProxy.java +++ b/server/src/main/java/cn/keking/web/filter/SecurityFilterProxy.java @@ -3,10 +3,10 @@ import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.OncePerRequestFilter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; diff --git a/server/src/main/java/cn/keking/web/filter/TrustDirFilter.java b/server/src/main/java/cn/keking/web/filter/TrustDirFilter.java index 431bb309e..f5c5c1f49 100644 --- a/server/src/main/java/cn/keking/web/filter/TrustDirFilter.java +++ b/server/src/main/java/cn/keking/web/filter/TrustDirFilter.java @@ -10,7 +10,7 @@ import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; -import javax.servlet.*; +import jakarta.servlet.*; import java.io.IOException; import java.net.URL; import java.net.URLDecoder; diff --git a/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java b/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java index e40120471..6282e162c 100644 --- a/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java +++ b/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java @@ -5,12 +5,12 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import org.apache.commons.collections4.CollectionUtils; import org.springframework.core.io.ClassPathResource; diff --git a/server/src/main/java/cn/keking/web/filter/UrlCheckFilter.java b/server/src/main/java/cn/keking/web/filter/UrlCheckFilter.java index 572486f91..e2d584e75 100644 --- a/server/src/main/java/cn/keking/web/filter/UrlCheckFilter.java +++ b/server/src/main/java/cn/keking/web/filter/UrlCheckFilter.java @@ -2,9 +2,9 @@ import org.apache.commons.lang3.StringUtils; -import javax.servlet.*; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; /** From 421640221bc7b7daed5398c3670bf65a820852be Mon Sep 17 00:00:00 2001 From: kl <632104866@QQ.com> Date: Sat, 11 Oct 2025 20:14:39 +0800 Subject: [PATCH 7/9] Kl (#688) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 升级: JDK1.8 升级到 JDK21 ,spring-boot 版本从 2.4.2 升级到 3.5.6 * 优化:启动日志新增 java version 输出信息 --- server/src/main/java/cn/keking/config/RedissonConfig.java | 5 ----- server/src/main/resources/banner.txt | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/server/src/main/java/cn/keking/config/RedissonConfig.java b/server/src/main/java/cn/keking/config/RedissonConfig.java index 6fb27acb2..4a701c240 100644 --- a/server/src/main/java/cn/keking/config/RedissonConfig.java +++ b/server/src/main/java/cn/keking/config/RedissonConfig.java @@ -50,26 +50,21 @@ Config config() throws Exception { .setConnectionMinimumIdleSize(connectionMinimumIdleSize) .setConnectionPoolSize(connectionPoolSize) .setDatabase(database) - .setDnsMonitoring(dnsMonitoring) .setDnsMonitoringInterval(dnsMonitoringInterval) .setSubscriptionConnectionMinimumIdleSize(subscriptionConnectionMinimumIdleSize) .setSubscriptionConnectionPoolSize(subscriptionConnectionPoolSize) .setSubscriptionsPerConnection(subscriptionsPerConnection) .setClientName(clientName) - .setFailedAttempts(failedAttempts) .setRetryAttempts(retryAttempts) .setRetryInterval(retryInterval) - .setReconnectionTimeout(reconnectionTimeout) .setTimeout(timeout) .setConnectTimeout(connectTimeout) .setIdleConnectionTimeout(idleConnectionTimeout) - .setPingTimeout(pingTimeout) .setPassword(StringUtils.trimToNull(password)); Codec codec=(Codec) ClassUtils.forName(getCodec(), ClassUtils.getDefaultClassLoader()).newInstance(); config.setCodec(codec); config.setThreads(thread); config.setEventLoopGroup(new NioEventLoopGroup()); - config.setUseLinuxNativeEpoll(false); return config; } diff --git a/server/src/main/resources/banner.txt b/server/src/main/resources/banner.txt index f394ae3e2..68c1fc906 100644 --- a/server/src/main/resources/banner.txt +++ b/server/src/main/resources/banner.txt @@ -6,6 +6,7 @@ | < | < | | | | | | | __/ \ / | | | __/ \ V V / |_|\_\ |_|\_\ |_| |_| |_| \___| \/ |_| \___| \_/\_/ + => Java Version :: ${java.version} => Spring Boot :: ${spring-boot.version} => kkFileView :: 4.4.0 => Home site :: https://kkview.cn From b1af0c7d721a0302a74c47b9864f674478806842 Mon Sep 17 00:00:00 2001 From: kl <632104866@QQ.com> Date: Mon, 13 Oct 2025 11:14:54 +0800 Subject: [PATCH 8/9] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=BE=93=E5=87=BA=E9=87=8D=E6=9E=84=20(#689)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/pom.xml | 9 +++++++++ server/src/main/config/application.properties | 8 ++++++++ .../java/cn/keking/service/FileConvertQueueTask.java | 2 +- .../java/cn/keking/service/FileHandlerService.java | 8 ++++---- .../cn/keking/service/impl/CadFilePreviewImpl.java | 5 ++++- .../cn/keking/service/impl/JsonFilePreviewImpl.java | 11 +++++------ .../cn/keking/service/impl/MediaFilePreviewImpl.java | 7 +++++-- .../keking/service/impl/SimTextFilePreviewImpl.java | 11 +++++------ server/src/main/java/cn/keking/utils/OfficeUtils.java | 7 +++++-- server/src/main/java/cn/keking/utils/RarUtils.java | 6 +++++- .../java/cn/keking/utils/SimpleEncodingDetects.java | 9 +++++++-- server/src/main/java/cn/keking/utils/WebUtils.java | 4 ++-- .../java/cn/keking/web/filter/TrustDirFilter.java | 2 +- .../java/cn/keking/web/filter/TrustHostFilter.java | 5 ++++- 14 files changed, 65 insertions(+), 29 deletions(-) diff --git a/server/pom.xml b/server/pom.xml index e4601a6f1..1f5e46a7e 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -50,6 +50,15 @@ org.springframework.boot spring-boot-starter-freemarker + + org.springframework.boot + spring-boot-starter-actuator + + + org.projectlombok + lombok + true + diff --git a/server/src/main/config/application.properties b/server/src/main/config/application.properties index 0099bc544..c8b6450d0 100644 --- a/server/src/main/config/application.properties +++ b/server/src/main/config/application.properties @@ -22,6 +22,14 @@ spring.freemarker.expose-request-attributes = true spring.freemarker.expose-session-attributes = true spring.freemarker.request-context-attribute = request spring.freemarker.suffix = .ftl +# Spring Boot Actuator 健康检查配置 +# 开启健康检查端点 +management.endpoints.web.exposure.include=health,info,metrics +# 显示详细的健康检查信息(生产环境建议设置为when-authorized) +management.endpoint.health.show-details=always +# 启用健康检查组件 +management.health.defaults.enabled=true + # office设置 #openoffice或LibreOffice home路径 diff --git a/server/src/main/java/cn/keking/service/FileConvertQueueTask.java b/server/src/main/java/cn/keking/service/FileConvertQueueTask.java index 3da38d3cf..6096b92bd 100644 --- a/server/src/main/java/cn/keking/service/FileConvertQueueTask.java +++ b/server/src/main/java/cn/keking/service/FileConvertQueueTask.java @@ -73,7 +73,7 @@ public void run() { TimeUnit.SECONDS.sleep(10); } catch (Exception ex) { Thread.currentThread().interrupt(); - ex.printStackTrace(); + logger.error("Failed to sleep after exception", ex); } logger.info("处理预览转换任务异常,url:{}", url, e); } diff --git a/server/src/main/java/cn/keking/service/FileHandlerService.java b/server/src/main/java/cn/keking/service/FileHandlerService.java index caecdccc6..a10a0819a 100644 --- a/server/src/main/java/cn/keking/service/FileHandlerService.java +++ b/server/src/main/java/cn/keking/service/FileHandlerService.java @@ -178,13 +178,13 @@ public void doActionConvertedFile(String outFilePath) { sb.append(""); sb.append(""); } catch (IOException e) { - e.printStackTrace(); + logger.error("Failed to read file: {}", outFilePath, e); } // 重新写入文件 try (FileOutputStream fos = new FileOutputStream(outFilePath); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos, StandardCharsets.UTF_8))) { writer.write(sb.toString()); } catch (IOException e) { - e.printStackTrace(); + logger.error("Failed to write file: {}", outFilePath, e); } } @@ -477,14 +477,14 @@ public FileAttribute getFileAttribute(String url, HttpServletRequest req) { originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名 attribute.setSkipDownLoad(true); } catch (UnsupportedEncodingException e) { - e.printStackTrace(); + logger.error("Failed to decode file name: {}", originFileName, e); } } if (UrlEncoderUtils.hasUrlEncoded(originFileName)) { //判断文件名是否转义 try { originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名 } catch (UnsupportedEncodingException e) { - e.printStackTrace(); + logger.error("Failed to decode file name: {}", originFileName, e); } }else { url = WebUtils.encodeUrlFileName(url); //对未转义的url进行转义 diff --git a/server/src/main/java/cn/keking/service/impl/CadFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/CadFilePreviewImpl.java index 155821c33..6f75fce70 100644 --- a/server/src/main/java/cn/keking/service/impl/CadFilePreviewImpl.java +++ b/server/src/main/java/cn/keking/service/impl/CadFilePreviewImpl.java @@ -9,6 +9,8 @@ import cn.keking.utils.KkFileUtils; import cn.keking.utils.WebUtils; import cn.keking.web.filter.BaseUrlFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.ui.Model; import org.springframework.util.StringUtils; @@ -22,6 +24,7 @@ @Service public class CadFilePreviewImpl implements FilePreview { + private static final Logger logger = LoggerFactory.getLogger(CadFilePreviewImpl.class); private static final String OFFICE_PREVIEW_TYPE_IMAGE = "image"; private static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages"; @@ -55,7 +58,7 @@ public String filePreviewHandle(String url, Model model, FileAttribute fileAttri try { imageUrls = fileHandlerService.cadToPdf(filePath, outFilePath, cadPreviewType, fileAttribute); } catch (Exception e) { - e.printStackTrace(); + logger.error("Failed to convert CAD file: {}", filePath, e); } if (imageUrls == null) { return otherFilePreview.notSupportedFile(model, fileAttribute, "CAD转换异常,请联系管理员"); diff --git a/server/src/main/java/cn/keking/service/impl/JsonFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/JsonFilePreviewImpl.java index b5d15714c..18e0f559d 100644 --- a/server/src/main/java/cn/keking/service/impl/JsonFilePreviewImpl.java +++ b/server/src/main/java/cn/keking/service/impl/JsonFilePreviewImpl.java @@ -7,6 +7,8 @@ import cn.keking.service.FilePreview; import cn.keking.utils.DownloadUtils; import cn.keking.utils.KkFileUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.binary.Base64; import org.springframework.stereotype.Service; import org.springframework.ui.Model; @@ -23,17 +25,14 @@ * @since 2025/01/11 * JSON 文件预览处理实现 */ +@Slf4j @Service +@RequiredArgsConstructor public class JsonFilePreviewImpl implements FilePreview { private final FileHandlerService fileHandlerService; private final OtherFilePreviewImpl otherFilePreview; - public JsonFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) { - this.fileHandlerService = fileHandlerService; - this.otherFilePreview = otherFilePreview; - } - @Override public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) { String fileName = fileAttribute.getName(); @@ -64,7 +63,7 @@ public String filePreviewHandle(String url, Model model, FileAttribute fileAttri try { fileData = HtmlUtils.htmlEscape(readJsonFile(filePath, fileName)); } catch (IOException e) { - e.printStackTrace(); + log.error("读取JSON文件失败: {}", filePath, e); } String base64Data = Base64.encodeBase64String(fileData.getBytes(StandardCharsets.UTF_8)); model.addAttribute("textData", base64Data); diff --git a/server/src/main/java/cn/keking/service/impl/MediaFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/MediaFilePreviewImpl.java index c0ada479b..608deae4e 100644 --- a/server/src/main/java/cn/keking/service/impl/MediaFilePreviewImpl.java +++ b/server/src/main/java/cn/keking/service/impl/MediaFilePreviewImpl.java @@ -11,6 +11,8 @@ import org.bytedeco.javacv.FFmpegFrameGrabber; import org.bytedeco.javacv.FFmpegFrameRecorder; import org.bytedeco.javacv.Frame; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.ui.Model; import org.springframework.util.ObjectUtils; @@ -26,6 +28,7 @@ @Service public class MediaFilePreviewImpl implements FilePreview { + private static final Logger logger = LoggerFactory.getLogger(MediaFilePreviewImpl.class); private final FileHandlerService fileHandlerService; private final OtherFilePreviewImpl otherFilePreview; private static final String mp4 = "mp4"; @@ -66,7 +69,7 @@ public String filePreviewHandle(String url, Model model, FileAttribute fileAttri convertedUrl = outFilePath; //其他协议的 不需要转换方式的文件 直接输出 } } catch (Exception e) { - e.printStackTrace(); + logger.error("Failed to convert media file: {}", filePath, e); } if (convertedUrl == null) { return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员"); @@ -148,7 +151,7 @@ private static String convertToMp4(String filePath, String outFilePath, FileAttr recorder.record(captured_frame); } } catch (Exception e) { - e.printStackTrace(); + logger.error("Failed to convert video file to mp4: {}", filePath, e); return null; } finally { if (recorder != null) { //关闭 diff --git a/server/src/main/java/cn/keking/service/impl/SimTextFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/SimTextFilePreviewImpl.java index ce7c6d677..64a8e9e56 100644 --- a/server/src/main/java/cn/keking/service/impl/SimTextFilePreviewImpl.java +++ b/server/src/main/java/cn/keking/service/impl/SimTextFilePreviewImpl.java @@ -8,6 +8,8 @@ import cn.keking.utils.DownloadUtils; import cn.keking.utils.EncodingDetects; import cn.keking.utils.KkFileUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.binary.Base64; import org.springframework.stereotype.Service; import org.springframework.ui.Model; @@ -20,17 +22,14 @@ * Created by kl on 2018/1/17. * Content :处理文本文件 */ +@Slf4j @Service +@RequiredArgsConstructor public class SimTextFilePreviewImpl implements FilePreview { private final FileHandlerService fileHandlerService; private final OtherFilePreviewImpl otherFilePreview; - public SimTextFilePreviewImpl(FileHandlerService fileHandlerService,OtherFilePreviewImpl otherFilePreview) { - this.fileHandlerService = fileHandlerService; - this.otherFilePreview = otherFilePreview; - } - @Override public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) { String fileName = fileAttribute.getName(); @@ -57,7 +56,7 @@ public String filePreviewHandle(String url, Model model, FileAttribute fileAttri try { fileData = HtmlUtils.htmlEscape(textData(filePath,fileName)); } catch (IOException e) { - e.printStackTrace(); + log.error("读取文本文件失败: {}", filePath, e); } model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes(StandardCharsets.UTF_8))); return TXT_FILE_PREVIEW_PAGE; diff --git a/server/src/main/java/cn/keking/utils/OfficeUtils.java b/server/src/main/java/cn/keking/utils/OfficeUtils.java index fb60423f6..ac828b677 100644 --- a/server/src/main/java/cn/keking/utils/OfficeUtils.java +++ b/server/src/main/java/cn/keking/utils/OfficeUtils.java @@ -4,6 +4,8 @@ import org.apache.poi.EncryptedDocumentException; import org.apache.poi.extractor.ExtractorFactory; import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; @@ -18,6 +20,7 @@ */ public class OfficeUtils { + private static final Logger logger = LoggerFactory.getLogger(OfficeUtils.class); private static final String POI_INVALID_PASSWORD_MSG = "password"; /** @@ -49,7 +52,7 @@ public static boolean isPwdProtected(String path) { try { propStream.close();//关闭文件输入流 } catch (IOException e) { - e.printStackTrace(); + logger.error("Failed to close input stream for file: {}", path, e); } } } @@ -76,7 +79,7 @@ public static synchronized boolean isCompatible(String path, String password) { try { propStream.close();//关闭文件输入流 } catch (IOException e) { - e.printStackTrace(); + logger.error("Failed to close input stream for file: {}", path, e); } } } diff --git a/server/src/main/java/cn/keking/utils/RarUtils.java b/server/src/main/java/cn/keking/utils/RarUtils.java index 795f91305..6a46d392e 100644 --- a/server/src/main/java/cn/keking/utils/RarUtils.java +++ b/server/src/main/java/cn/keking/utils/RarUtils.java @@ -1,6 +1,9 @@ package cn.keking.utils; import cn.keking.config.ConfigConstants; import cn.keking.service.ZtreeNodeVo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.File; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; @@ -15,6 +18,7 @@ * create : 2023-04-08 **/ public class RarUtils { + private static final Logger logger = LoggerFactory.getLogger(RarUtils.class); private static final String fileDir = ConfigConstants.getFileDir(); public static byte[] getUTF8BytesFromGBKString(String gbkStr) { @@ -55,7 +59,7 @@ public static String getUtf8String(String str) { str = new String(getUTF8BytesFromGBKString(str), StandardCharsets.UTF_8); } } catch (UnsupportedEncodingException e) { - e.printStackTrace(); + logger.error("Failed to convert string encoding: {}", str, e); } } return str; diff --git a/server/src/main/java/cn/keking/utils/SimpleEncodingDetects.java b/server/src/main/java/cn/keking/utils/SimpleEncodingDetects.java index 19e138bb7..5b1aec4c3 100644 --- a/server/src/main/java/cn/keking/utils/SimpleEncodingDetects.java +++ b/server/src/main/java/cn/keking/utils/SimpleEncodingDetects.java @@ -1,5 +1,8 @@ package cn.keking.utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -31,6 +34,8 @@ */ public class SimpleEncodingDetects { + private static final Logger logger = LoggerFactory.getLogger(SimpleEncodingDetects.class); + /** * 得到文件的编码 * @param content 文件内容 @@ -65,10 +70,10 @@ public static void readFile(String file, String code) { } catch (FileNotFoundException e) { // TODO Auto-generated catch block - e.printStackTrace(); + logger.error("File not found: {}", file, e); } catch (IOException e) { // TODO Auto-generated catch block - e.printStackTrace(); + logger.error("Failed to read file: {}", file, e); } } diff --git a/server/src/main/java/cn/keking/utils/WebUtils.java b/server/src/main/java/cn/keking/utils/WebUtils.java index 53cedb888..f62862cfb 100644 --- a/server/src/main/java/cn/keking/utils/WebUtils.java +++ b/server/src/main/java/cn/keking/utils/WebUtils.java @@ -87,7 +87,7 @@ public static String urlEncoderencode(String urlStr) { try { urlStr = URLEncoder.encode(urlStr, "UTF-8").replaceAll("\\+", "%20").replaceAll("%3A", ":").replaceAll("%2F", "/").replaceAll("%3F", "?").replaceAll("%26", "&").replaceAll("%3D", "="); } catch (UnsupportedEncodingException e) { - e.printStackTrace(); + LOGGER.error("Failed to encode URL: {}", urlStr, e); } } return urlStr; @@ -155,7 +155,7 @@ public static String getFileNameFromURL(String url) { URL urlObj = new URL(url); url = urlObj.getPath().substring(1); } catch (MalformedURLException e) { - e.printStackTrace(); + LOGGER.error("Failed to parse file URL: {}", url, e); } } // 因为url的参数中可能会存在/的情况,所以直接url.lastIndexOf("/")会有问题 diff --git a/server/src/main/java/cn/keking/web/filter/TrustDirFilter.java b/server/src/main/java/cn/keking/web/filter/TrustDirFilter.java index f5c5c1f49..c71a21eca 100644 --- a/server/src/main/java/cn/keking/web/filter/TrustDirFilter.java +++ b/server/src/main/java/cn/keking/web/filter/TrustDirFilter.java @@ -35,7 +35,7 @@ public void init(FilterConfig filterConfig) { byte[] bytes = FileCopyUtils.copyToByteArray(classPathResource.getInputStream()); this.notTrustDirView = new String(bytes, StandardCharsets.UTF_8); } catch (IOException e) { - e.printStackTrace(); + logger.error("加载notTrustDir.html失败", e); } } diff --git a/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java b/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java index 6282e162c..067a347e6 100644 --- a/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java +++ b/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java @@ -13,6 +13,8 @@ import jakarta.servlet.ServletResponse; import org.apache.commons.collections4.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.core.io.ClassPathResource; import org.springframework.util.FileCopyUtils; @@ -22,6 +24,7 @@ */ public class TrustHostFilter implements Filter { + private static final Logger logger = LoggerFactory.getLogger(TrustHostFilter.class); private String notTrustHostHtmlView; @Override @@ -32,7 +35,7 @@ public void init(FilterConfig filterConfig) { byte[] bytes = FileCopyUtils.copyToByteArray(classPathResource.getInputStream()); this.notTrustHostHtmlView = new String(bytes, StandardCharsets.UTF_8); } catch (IOException e) { - e.printStackTrace(); + logger.error("Failed to load notTrustHost.html file", e); } } From 9f3b45a4c74e501695bffd851b60e920980fd4a8 Mon Sep 17 00:00:00 2001 From: kl <632104866@QQ.com> Date: Mon, 20 Oct 2025 14:29:05 +0800 Subject: [PATCH 9/9] =?UTF-8?q?=E5=AE=89=E5=85=A8=EF=BC=9A=E5=BC=BA?= =?UTF-8?q?=E5=88=B6=E7=94=A8=E6=88=B7=E9=85=8D=E7=BD=AE=E5=8F=AF=E8=AE=BF?= =?UTF-8?q?=E9=97=AE=E5=9F=9F=E5=90=8D=E7=9A=84=E7=99=BD=E5=90=8D=E5=8D=95?= =?UTF-8?q?=E6=88=96=E8=80=85=E9=BB=91=E5=90=8D=E5=8D=95=EF=BC=8C=E6=8F=90?= =?UTF-8?q?=E9=AB=98=E5=AE=89=E5=85=A8=E6=80=A7=20(#692)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 安全:强制用户配置可访问域名的白名单或者黑名单,提高安全性 * 安全:强制用户配置可访问域名的白名单或者黑名单,提高安全性 * CI:修复 CI 问题 * CI:修复 CI 问题 --- .github/workflows/maven.yml | 40 ++++- SECURITY_CONFIG.md | 170 ++++++++++++++++++ server/src/main/config/application.properties | 22 ++- .../cn/keking/config/ConfigConstants.java | 3 +- .../cn/keking/web/filter/TrustHostFilter.java | 14 +- 5 files changed, 237 insertions(+), 12 deletions(-) create mode 100644 SECURITY_CONFIG.md diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index db909c6bd..379af2233 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -12,13 +12,41 @@ on: jobs: build: runs-on: ubuntu-22.04 + steps: - - uses: actions/checkout@v2 - - name: Set up JDK 8 - uses: actions/setup-java@v2 + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 with: - java-version: '8' - distribution: 'adopt' - cache: maven + java-version: '21' + distribution: 'temurin' # 使用 Eclipse Temurin (AdoptOpenJDK 的继任者) + cache: 'maven' + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Build with Maven run: mvn -B package -Dmaven.test.skip=true --file pom.xml + + - name: Upload Linux distribution package + if: success() + uses: actions/upload-artifact@v4 + with: + name: kkfileview-linux + path: server/target/*.tar.gz + retention-days: 7 + + - name: Upload Windows distribution package + if: success() + uses: actions/upload-artifact@v4 + with: + name: kkfileview-windows + path: server/target/*.zip + retention-days: 7 diff --git a/SECURITY_CONFIG.md b/SECURITY_CONFIG.md new file mode 100644 index 000000000..ee87b300a --- /dev/null +++ b/SECURITY_CONFIG.md @@ -0,0 +1,170 @@ +# kkFileView 安全配置指南 + +## ⚠️ 重要安全更新 + +从 4.4.0 之后版本开始,kkFileView 增强了安全性,默认拒绝所有未配置的外部文件预览请求,以防止 SSRF(服务器端请求伪造)攻击。 + +## 🔒 安全配置说明 + +### 1. 信任主机白名单配置(推荐) + +在 `application.properties` 中配置允许预览的域名: + +```properties +# 方式1:通过配置文件 +trust.host = kkview.cn,yourdomain.com,cdn.example.com + +# 方式2:通过环境变量 +KK_TRUST_HOST=kkview.cn,yourdomain.com,cdn.example.com +``` + +**示例场景**: +- 只允许预览来自 `oss.aliyuncs.com` 和 `cdn.example.com` 的文件 +```properties +trust.host = oss.aliyuncs.com,cdn.example.com +``` + +### 2. 允许所有主机(不推荐,仅测试环境) + +```properties +trust.host = * +``` + +⚠️ **警告**:此配置会允许访问任意外部地址,存在安全风险,仅应在测试环境使用! + +### 3. 黑名单配置(高级) + +禁止特定域名或内网地址: + +```properties +# 禁止访问内网地址(强烈推荐) +not.trust.host = localhost,127.0.0.1,192.168.*,10.*,172.16.*,169.254.* + +# 禁止特定恶意域名 +not.trust.host = malicious-site.com,spam-domain.net +``` + +**优先级**:黑名单 > 白名单 + +### 4. Docker 环境配置 + +```bash +docker run -d \ + -e KK_TRUST_HOST=yourdomain.com,cdn.example.com \ + -e KK_NOT_TRUST_HOST=localhost,127.0.0.1 \ + -p 8012:8012 \ + keking/kkfileview:4.4.0 +``` + +## 🛡️ 安全最佳实践 + +### ✅ 推荐配置 + +```properties +# 1. 明确配置信任主机白名单 +trust.host = your-cdn.com,your-storage.com + +# 2. 配置黑名单防止内网访问 +not.trust.host = localhost,127.0.0.1,192.168.*,10.*,172.16.* + +# 3. 禁用文件上传(生产环境) +file.upload.disable = true + +# 4. 配置基础URL(使用反向代理时) +base.url = https://preview.yourdomain.com +``` + +### ❌ 不推荐配置 + +```properties +# 危险:允许所有主机访问 +trust.host = * + +# 危险:启用文件上传(生产环境) +file.upload.disable = false +``` + +## 🔍 配置验证 + +### 测试白名单是否生效 + +1. 配置白名单: +```properties +trust.host = kkview.cn +``` + +2. 尝试预览白名单内的文件: +``` +http://localhost:8012/onlinePreview?url=https://kkview.cn/test.pdf +✅ 应该可以正常预览 +``` + +3. 尝试预览白名单外的文件: +``` +http://localhost:8012/onlinePreview?url=https://other-domain.com/test.pdf +❌ 应该被拒绝,显示"不信任的文件源" +``` + +### 测试黑名单是否生效 + +1. 配置黑名单: +```properties +not.trust.host = localhost,127.0.0.1 +``` + +2. 尝试访问本地文件: +``` +http://localhost:8012/getCorsFile?urlPath=http://127.0.0.1:8080/admin +❌ 应该被拒绝 +``` + +## 📋 常见问题 + +### Q1: 升级后无法预览文件了? + +**原因**:新版本默认拒绝未配置的主机。 + +**解决**:在配置文件中添加信任主机列表: +```properties +trust.host = your-file-server.com +``` + +### Q2: 如何临时恢复旧版本行为? + +**不推荐**,但如果确实需要: +```properties +trust.host = * +``` + +### Q3: 配置了白名单但还是无法访问? + +检查以下几点: +1. 域名是否完全匹配(区分大小写) +2. 是否配置了黑名单,黑名单优先级更高 +3. 查看日志中的 WARNING 信息 +4. 确认环境变量是否正确设置 + +### Q4: 如何允许子域名? + +目前不支持通配符域名匹配,需要明确列出每个子域名: +```properties +trust.host = cdn.example.com,api.example.com,storage.example.com +``` + +## 🚨 安全事件响应 + +如果发现可疑的预览请求: + +1. 检查日志文件,搜索 "拒绝访问主机" 关键字 +2. 确认 `trust.host` 配置是否合理 +3. 检查是否有异常的网络请求 +4. 如发现攻击行为,及时更新黑名单配置 + +## 📞 获取帮助 + +- GitHub Issues: https://github.com/kekingcn/kkFileView/issues +- Gitee Issues: https://gitee.com/kekingcn/file-online-preview/issues + +--- + +**安全提示**:定期检查和更新信任主机列表,遵循最小权限原则。 diff --git a/server/src/main/config/application.properties b/server/src/main/config/application.properties index c8b6450d0..54854c037 100644 --- a/server/src/main/config/application.properties +++ b/server/src/main/config/application.properties @@ -86,11 +86,25 @@ cache.clean.cron = ${KK_CACHE_CLEAN_CRON:0 0 3 * * ?} #提供预览服务的地址,默认从请求url读,如果使用nginx等反向代理,需要手动设置 #base.url = https://file.keking.cn base.url = ${KK_BASE_URL:default} -#信任站点,多个用','隔开,设置了之后,会限制只能预览来自信任站点列表的文件,默认不限制 -#trust.host = kkview.cn + +# ========== 安全配置(重要)========== +# 信任站点白名单配置,多个用','隔开 +# ⚠️ 安全提示:为防止SSRF攻击,强烈建议配置信任主机白名单 +# ⚠️ 如果不配置,系统将默认拒绝所有外部文件预览请求 +# +# 配置示例: +# trust.host = kkview.cn,yourdomain.com,cdn.example.com +# +# 如果需要允许所有域名(不推荐,仅用于测试环境),请设置为: +# trust.host = * +# +# 当前配置: trust.host = ${KK_TRUST_HOST:default} -#不信任站点,多个用','隔开,设置了之后,会限制来自不信任站点列表的文件,默认不限制 -#not.trust.host = kkview.cn + +# 不信任站点黑名单配置,多个用','隔开 +# 黑名单优先级高于白名单,设置后将禁止预览来自这些站点的文件 +# 建议配置:禁止访问内网地址和本地地址 +# not.trust.host = localhost,127.0.0.1,0.0.0.0,192.168.*,10.*,172.16.* not.trust.host= ${KK_NOT_TRUST_HOST:default} #文本类型,默认如下,可自定义添加 simText = ${KK_SIMTEXT:txt,html,htm,asp,jsp,xml,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd} diff --git a/server/src/main/java/cn/keking/config/ConfigConstants.java b/server/src/main/java/cn/keking/config/ConfigConstants.java index 9310441c7..69fd600ae 100644 --- a/server/src/main/java/cn/keking/config/ConfigConstants.java +++ b/server/src/main/java/cn/keking/config/ConfigConstants.java @@ -308,7 +308,8 @@ private static CopyOnWriteArraySet getHostValue(String trustHost) { if (DEFAULT_VALUE.equalsIgnoreCase(trustHost)) { return new CopyOnWriteArraySet<>(); } else { - String[] trustHostArray = trustHost.toLowerCase().split(","); + // 去除空格并转小写 + String[] trustHostArray = trustHost.toLowerCase().replaceAll("\\s+", "").split(","); return new CopyOnWriteArraySet<>(Arrays.asList(trustHostArray)); } } diff --git a/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java b/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java index 067a347e6..e661844f4 100644 --- a/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java +++ b/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java @@ -54,13 +54,25 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } public boolean isNotTrustHost(String host) { + // 如果配置了黑名单,优先检查黑名单 if (CollectionUtils.isNotEmpty(ConfigConstants.getNotTrustHostSet())) { return ConfigConstants.getNotTrustHostSet().contains(host); } + + // 如果配置了白名单,检查是否在白名单中 if (CollectionUtils.isNotEmpty(ConfigConstants.getTrustHostSet())) { + // 支持通配符 * 表示允许所有主机 + if (ConfigConstants.getTrustHostSet().contains("*")) { + logger.debug("允许所有主机访问(通配符模式): {}", host); + return false; + } return !ConfigConstants.getTrustHostSet().contains(host); } - return false; + + // 安全加固:默认拒绝所有未配置的主机(防止SSRF攻击) + // 如果需要允许所有主机,请在配置文件中明确设置 trust.host = * + logger.warn("未配置信任主机列表,拒绝访问主机: {},请在配置文件中设置 trust.host 或 KK_TRUST_HOST 环境变量", host); + return true; } @Override