From 1f60e30f0921aeebd6afc361ccbd7607460e5b78 Mon Sep 17 00:00:00 2001 From: gaoxingzaq <63081664+gaoxingzaq@users.noreply.github.com> Date: Sat, 9 May 2026 15:04:25 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E5=8C=BA=E5=88=86?= =?UTF-8?q?=20HTTP=20=E9=94=99=E8=AF=AF=EF=BC=8C=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E6=98=8E=E7=A1=AE=E5=8E=9F=E5=9B=A0=20(#752)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 优化:区分 HTTP 错误,返回明确原因 * 修复 缓存关闭共享的 client,会导致后续请求失败 * 修复 缓存关闭共享的 client,会导致后续请求失败 --------- Co-authored-by: 高雄 --- .../java/cn/keking/utils/DownloadUtils.java | 50 +++++++++---- .../cn/keking/utils/HttpClientLifecycle.java | 14 ++++ .../controller/OnlinePreviewController.java | 72 ++++++++++++++++--- 3 files changed, 113 insertions(+), 23 deletions(-) create mode 100644 server/src/main/java/cn/keking/utils/HttpClientLifecycle.java diff --git a/server/src/main/java/cn/keking/utils/DownloadUtils.java b/server/src/main/java/cn/keking/utils/DownloadUtils.java index 42ea2f18..bf9fdfbb 100644 --- a/server/src/main/java/cn/keking/utils/DownloadUtils.java +++ b/server/src/main/java/cn/keking/utils/DownloadUtils.java @@ -8,6 +8,7 @@ import org.apache.commons.io.FileUtils; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.web.client.HttpClientErrorException; import java.io.File; import java.io.FileNotFoundException; @@ -46,9 +47,8 @@ public class DownloadUtils { } ReturnResponse response = new ReturnResponse<>(0, "下载成功!!!", ""); String realPath = getRelFilePath(fileName, fileAttribute); - // 获取文件后缀用于校验 final String fileSuffix = fileAttribute.getSuffix(); - // 判断是否非法地址 + if (KkFileUtils.isIllegalFileName(realPath)) { response.setCode(1); response.setContent(null); @@ -61,17 +61,17 @@ public class DownloadUtils { response.setMsg("下载失败:不支持的类型!" + urlStr); return response; } - if (fileAttribute.isCompressFile()) { //压缩包文件 直接赋予路径 不予下载 + if (fileAttribute.isCompressFile()) { response.setContent(fileDir + fileName); response.setMsg(fileName); return response; } - // 如果文件是否已经存在、且不强制更新,则直接返回文件路径 if (KkFileUtils.isExist(realPath) && !fileAttribute.forceUpdatedCache()) { response.setContent(realPath); response.setMsg(fileName); return response; } + try { URL url = WebUtils.normalizedURL(urlStr); if (!fileAttribute.getSkipDownLoad()) { @@ -79,39 +79,59 @@ public class DownloadUtils { File realFile = new File(realPath); CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient(); String finalUrlStr = urlStr; - HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> { - // 获取响应头中的Content-Type - String contentType = responseWrapper.getContentType(); - // 如果是Office/设计文件,需要校验MIME类型 + final boolean[] hasMimeError = {false}; + final String[] mimeErrorMessage = {null}; + + HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> { + String contentType = responseWrapper.getContentType(); if (WebUtils.isMimeCheckRequired(fileSuffix)) { if (!WebUtils.isValidMimeType(contentType, fileSuffix)) { logger.error("文件类型错误,期望二进制文件但接收到文本类型,url: {}, Content-Type: {}", finalUrlStr, contentType); - responseWrapper.setHasError(true); + hasMimeError[0] = true; + mimeErrorMessage[0] = "期望二进制文件但接收到文本类型,Content-Type: " + contentType; return; } } - - // 保存文件 FileUtils.copyToFile(responseWrapper.getInputStream(), realFile); }); + + if (hasMimeError[0]) { + response.setCode(1); + response.setContent(null); + response.setMsg(mimeErrorMessage[0]); + return response; + } + } else if (isFtpUrl(url)) { String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME); String ftpPassword = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_PASSWORD); String ftpControlEncoding = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_CONTROL_ENCODING); String ftpport = WebUtils.getUrlParameterReg(realPath, URL_PARAM_FTP_PORT); FtpUtils.download(fileAttribute.getUrl(), ftpport, realPath, ftpUsername, ftpPassword, ftpControlEncoding); - } else if (isFileUrl(url)) { // 添加对file协议的支持 + } else if (isFileUrl(url)) { handleFileProtocol(url, realPath); } else { response.setCode(1); response.setMsg("url不能识别url" + urlStr); + return response; } } response.setContent(realPath); response.setMsg(fileName); return response; + + } catch (HttpClientErrorException e) { + logger.error("HTTP请求失败,状态码:{},url:{}", e.getStatusCode(), urlStr); + response.setCode(1); + response.setContent(null); + if (e.getStatusCode().is4xxClientError()) { + response.setMsg("文件不存在或无法访问 (HTTP " + e.getStatusCode() + ")"); + } else { + response.setMsg("下载失败: " + e.getMessage()); + } + return response; } catch (IOException | GalimatiasParseException e) { logger.error("文件下载失败,url:{}", urlStr); response.setCode(1); @@ -123,7 +143,11 @@ public class DownloadUtils { } return response; } catch (Exception e) { - throw new RuntimeException(e); + logger.error("下载文件时发生未知异常,url:{}", urlStr, e); + response.setCode(1); + response.setContent(null); + response.setMsg("下载失败: " + e.getMessage()); + return response; } } diff --git a/server/src/main/java/cn/keking/utils/HttpClientLifecycle.java b/server/src/main/java/cn/keking/utils/HttpClientLifecycle.java new file mode 100644 index 00000000..6142ff96 --- /dev/null +++ b/server/src/main/java/cn/keking/utils/HttpClientLifecycle.java @@ -0,0 +1,14 @@ +package cn.keking.utils; + +import org.springframework.stereotype.Component; +import jakarta.annotation.PreDestroy; + +@Component +public class HttpClientLifecycle { + + @PreDestroy + public void destroy() { + System.out.println("Spring 容器关闭,释放 HTTP 连接池资源..."); + HttpRequestUtils.shutdown(); + } +} \ No newline at end of file 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 d772ced1..666e322b 100644 --- a/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java +++ b/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java @@ -23,6 +23,8 @@ import org.springframework.web.bind.annotation.ResponseBody; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.client.HttpClientErrorException; + import java.io.IOException; import java.io.InputStream; import java.net.URL; @@ -152,34 +154,71 @@ public class OnlinePreviewController { // 1. 验证接口是否开启 if (!ConfigConstants.getGetCorsFile()) { logger.info("接口关闭,禁止访问!,url:{}", urlPath); + try { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "接口已关闭"); + } catch (IOException ignored) {} return; } - //2. 验证访问权限 + // 2. 验证访问权限 if (WebUtils.validateKey(key)) { logger.info("访问不合法:访问密码不正确!,url:{}", urlPath); + try { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "访问密码不正确"); + } catch (IOException ignored) {} return; } + URL url; try { urlPath = WebUtils.decodeUrl(urlPath, encryption); url = WebUtils.normalizedURL(urlPath); } catch (Exception ex) { - logger.error(String.format(BASE64_DECODE_ERROR_MSG, urlPath),ex); + logger.error(String.format(BASE64_DECODE_ERROR_MSG, urlPath), ex); + try { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "URL 解析失败"); + } catch (IOException ignored) {} return; } + assert urlPath != null; if (!isHttpUrl(url) && !isFtpUrl(url)) { logger.info("读取跨域文件异常,可能存在非法访问,urlPath:{}", urlPath); + try { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "不支持的协议"); + } catch (IOException ignored) {} return; } - FileAttribute fileAttribute = fileHandlerService.getFileAttribute(urlPath, req); - InputStream inputStream = null; - logger.info("读取跨域文件url:{}", urlPath); - if (!isFtpUrl(url)) { - CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient(); - HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> IOUtils.copy(responseWrapper.getInputStream(), response.getOutputStream())); + FileAttribute fileAttribute = fileHandlerService.getFileAttribute(urlPath, req); + logger.info("读取跨域文件url:{}", urlPath); + + if (!isFtpUrl(url)) { + // HTTP/HTTPS 处理(修复:不关闭共享的 CloseableHttpClient) + CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient(); + try { + HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> IOUtils.copy(responseWrapper.getInputStream(), response.getOutputStream())); + } catch (HttpClientErrorException e) { + // 捕获 HTTP 4xx 错误(如 404) + logger.error("HTTP 请求失败,状态码:{},url:{}", e.getStatusCode(), urlPath); + try { + if (e.getStatusCode().is4xxClientError()) { + response.sendError(e.getStatusCode().value(), "文件不存在或无法访问"); + } else { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "下载文件时发生错误"); + } + } catch (IOException ignored) { + } + } catch (Exception e) { + // 捕获其他异常(如连接超时、IO 异常等) + logger.error("读取跨域文件异常,url:{}", urlPath, e); + try { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "读取文件失败: " + e.getMessage()); + } catch (IOException ignored) { + } + } } else { + // FTP 处理 + InputStream inputStream = null; try { String filename = urlPath.substring(urlPath.lastIndexOf('/') + 1); String contentType = WebUtils.getContentTypeByFilename(filename); @@ -190,10 +229,23 @@ public class OnlinePreviewController { String ftpPassword = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_PASSWORD); String ftpControlEncoding = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_CONTROL_ENCODING); String support = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_PORT); - inputStream= FtpUtils.preview(urlPath,support, urlPath, ftpUsername, ftpPassword, ftpControlEncoding); + inputStream = FtpUtils.preview(urlPath, support, urlPath, ftpUsername, ftpPassword, ftpControlEncoding); IOUtils.copy(inputStream, response.getOutputStream()); } catch (IOException e) { - logger.error("读取跨域文件异常,url:{}", urlPath); + logger.error("读取跨域文件异常,url:{}", urlPath, e); + try { + // 根据异常信息判断是否为文件不存在 + if (e.getMessage() != null && (e.getMessage().contains("550") || e.getMessage().contains("File not found"))) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "FTP 文件不存在"); + } else { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "FTP 读取失败"); + } + } catch (IOException ignored) {} + } catch (Exception e) { + logger.error("FTP 预览发生未知异常,url:{}", urlPath, e); + try { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "FTP 服务异常"); + } catch (IOException ignored) {} } finally { IOUtils.closeQuietly(inputStream); }