diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index c7c179272..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 --file pom.xml + 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/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..1f5e46a7e 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -44,20 +44,20 @@ org.springframework.boot spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-tomcat - - + org.springframework.boot - spring-boot-starter-jetty + spring-boot-starter-freemarker org.springframework.boot - spring-boot-starter-freemarker + spring-boot-starter-actuator + + + org.projectlombok + lombok + true @@ -100,9 +100,8 @@ - org.apache.httpcomponents - httpclient - ${httpcomponents.version} + org.apache.httpcomponents.client5 + httpclient5 @@ -327,6 +326,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/config/application.properties b/server/src/main/config/application.properties index 251f5d691..54854c037 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路径 @@ -78,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} @@ -159,10 +181,9 @@ watermark.height = ${WATERMARK_HEIGHT:80} #水印倾斜度数,要求设置在大于等于0,小于90 watermark.angle = ${WATERMARK_ANGLE:10} - #首页功能设置 #是否禁用首页文件上传 -file.upload.disable = ${KK_FILE_UPLOAD_DISABLE:false} +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..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)); } } @@ -426,7 +427,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/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/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/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/FileConvertQueueTask.java b/server/src/main/java/cn/keking/service/FileConvertQueueTask.java index e5d624853..6096b92bd 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; /** @@ -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 2444bc750..a10a0819a 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; @@ -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/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/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/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 new file mode 100644 index 000000000..18e0f559d --- /dev/null +++ b/server/src/main/java/cn/keking/service/impl/JsonFilePreviewImpl.java @@ -0,0 +1,90 @@ +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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) + * @since 2025/01/11 + * JSON 文件预览处理实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class JsonFilePreviewImpl implements FilePreview { + + private final FileHandlerService fileHandlerService; + private final OtherFilePreviewImpl otherFilePreview; + + @Override + public String filePreviewHandle(String url, Model model, FileAttribute 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) { + log.error("读取JSON文件失败: {}", filePath, e); + } + 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/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 9543abb5b..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(); @@ -47,7 +46,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()); } @@ -57,9 +56,9 @@ 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())); + 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/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/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 de14fb642..f62862cfb 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; @@ -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("/")会有问题 @@ -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..c71a21eca 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; @@ -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 e40120471..e661844f4 100644 --- a/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java +++ b/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java @@ -5,14 +5,16 @@ 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.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); } } @@ -51,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 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; /** 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 diff --git a/server/src/main/resources/web/json.ftl b/server/src/main/resources/web/json.ftl new file mode 100644 index 000000000..16a64fb63 --- /dev/null +++ b/server/src/main/resources/web/json.ftl @@ -0,0 +1,384 @@ + + + + + + JSON文件预览 + <#include "*/commonHeader.ftl"> + + + + + + + + + +
+
+ + +
+
+
+
+ + + + + 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 +
+ 请注意:文件上传限开发环境调试使用,生产环境建议保持关闭状态,避免非法上传导致的安全隐患。 +

+