Compare commits

..

16 Commits

Author SHA1 Message Date
92ca92bee6 支持Java25环境 2025-12-03 18:46:23 +08:00
64c82a2406 !334 update docker/kkfileview-base/Dockerfile.
Merge pull request !334 from 云上小朱/N/A
2025-11-17 00:53:58 +00:00
e44ef813a1 update docker/kkfileview-base/Dockerfile.
在文件docker/kkfileview-base/Dockerfile中
我看到POM里写的Java21,但是Dockerfile写的JDK8. 是否应改为JDK21,我在本地测试,改为JDK21可正常打包代码并正常运行。

Signed-off-by: 云上小朱 <13077784+ysxz2025@user.noreply.gitee.com>
2025-11-14 01:04:31 +00:00
kl
9f3b45a4c7 安全:强制用户配置可访问域名的白名单或者黑名单,提高安全性 (#692)
* 安全:强制用户配置可访问域名的白名单或者黑名单,提高安全性

* 安全:强制用户配置可访问域名的白名单或者黑名单,提高安全性

* CI:修复 CI 问题

* CI:修复 CI 问题
2025-10-20 14:29:05 +08:00
kl
b1af0c7d72 优化:日志输出重构 (#689) 2025-10-13 11:14:54 +08:00
kl
421640221b Kl (#688)
* 升级: JDK1.8 升级到 JDK21 ,spring-boot 版本从 2.4.2 升级到 3.5.6

* 优化:启动日志新增 java version 输出信息
2025-10-11 20:14:39 +08:00
kl
f6c6e22b0d 升级: JDK1.8 升级到 JDK21 ,spring-boot 版本从 2.4.2 升级到 3.5.6 (#687) 2025-10-11 20:05:43 +08:00
kl
51653483b9 Kl json (#686)
* 新增:JSON 文件格式化预览功能

* 优化:优化 JSON 文件格式化预览效果
2025-10-11 17:54:17 +08:00
kl
a9787b0add 新增:JSON 文件格式化预览功能 (#685) 2025-10-11 11:52:49 +08:00
kl
6cdbf92fb0 优化:完善文件上传禁用功能的用户体验 (#684) 2025-10-11 10:41:34 +08:00
kl
fdb40680d3 安全:禁用文件上传接口 (#656)
- 从配置文件中移除file.upload.disable配置项
- 删除FileController中的文件上传相关代码
- 移除/fileUpload POST接口
- 删除文件上传校验逻辑
- 增强系统安全性,防止恶意文件上传

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-06-30 17:57:45 +08:00
6746325bf2 update Github workflow 2025-04-14 16:10:29 +08:00
05a8bff1e0 优化:调整单元测试 2025-03-31 15:56:50 +08:00
874ff5b3f6 优化:解决部分场景下, 页面元素变化导致水印覆盖不全问题 2025-03-31 15:28:36 +08:00
2230cfa52b update README.cn.md 2025-01-22 11:08:28 +08:00
83c581364d update README.cn.md 2025-01-22 11:04:40 +08:00
60 changed files with 955 additions and 134 deletions

View File

@ -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

View File

@ -53,80 +53,81 @@
#### 1. 文本预览
支持所有类型的文本文档预览 由于文本文档类型过多无法全部枚举默认开启的类型如下 txt,html,htm,asp,jsp,xml,xbrl,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd
文本预览效果如下
![文本预览效果如下](https://kkview.cn/img/preview/preview-text.png)
![文本预览效果如下](./doc/img/preview/preview-text.png)
#### 2. 图片预览
支持jpgjpegpnggif等图片预览翻转缩放镜像预览效果如下
![图片预览](https://kkview.cn/img/preview/preview-image.png)
![图片预览](./doc/img/preview/preview-image.png)
#### 3. word文档预览
支持docdocx文档预览word预览有两种模式一种是每页word转为图片预览另一种是整个word文档转成pdf再预览pdf两种模式的适用场景如下
* 图片预览word文件大前台加载整个pdf过慢
* pdf预览内网访问加载pdf快
图片预览模式预览效果如下
![word文档预览1](https://kkview.cn/img/preview/preview-doc-image.png)
![word文档预览1](./doc/img/preview/preview-doc-image.png)
pdf预览模式预览效果如下
![word文档预览2](https://kkview.cn/img/preview/preview-doc-pdf.png)
![word文档预览2](./doc/img/preview/preview-doc-pdf.png)
#### 4. ppt文档预览
支持pptpptx文档预览和word文档一样有两种预览模式
图片预览模式预览效果如下
![ppt文档预览1](https://kkview.cn/img/preview/preview-ppt-image.png)
![ppt文档预览1](./doc/img/preview/preview-ppt-image.png)
pdf预览模式预览效果如下
![ppt文档预览2](https://kkview.cn/img/preview/preview-ppt-pdf.png)
![ppt文档预览2](./doc/img/preview/preview-ppt-pdf.png)
#### 5. pdf文档预览
支持pdf文档预览和word文档一样有两种预览模式
图片预览模式预览效果如下
![pdf文档预览1](https://kkview.cn/img/preview/preview-pdf-image.png)
![pdf文档预览1](./doc/img/preview/preview-pdf-image.png)
pdf预览模式预览效果如下
![pdf文档预览2](https://kkview.cn/img/preview/preview-pdf-pdf.png)
![pdf文档预览2](./doc/img/preview/preview-pdf-pdf.png)
#### 6. excel文档预览
支持xlsxlsx文档预览预览效果如下
![excel文档预览](https://kkview.cn/img/preview/preview-xls.png)
![excel文档预览](./doc/img/preview/preview-xls.png)
#### 7. 压缩文件预览
支持zip,rar,jar,tar,gzip等压缩包预览效果如下
![压缩文件预览1](https://kkview.cn/img/preview/preview-zip.png)
![压缩文件预览1](./doc/img/preview/preview-zip.png)
可点击压缩包中的文件名直接预览文件预览效果如下
![压缩文件预览2](https://kkview.cn/img/preview/preview-zip-inner.png)
![压缩文件预览2](./doc/img/preview/preview-zip-inner.png)
#### 8. 多媒体文件预览
理论上支持所有的视频音频文件由于无法枚举所有文件格式默认开启的类型如下
mp3,wav,mp4,flv
视频预览效果如下
![多媒体文件预览1](https://kkview.cn/img/preview/preview-video.png)
![多媒体文件预览1](./doc/img/preview/preview-video.png)
音频预览效果如下
![多媒体文件预览2](https://kkview.cn/img/preview/preview-audio.png)
![多媒体文件预览2](./doc/img/preview/preview-audio.png)
#### 9. CAD文档预览
支持CAD dwg文档预览和word文档一样有两种预览模式
图片预览模式预览效果如下
![cad文档预览1](https://kkview.cn/img/preview/preview-cad-image.png)
![cad文档预览1](./doc/img/preview/preview-cad-image.png)
pdf预览模式预览效果如下
![cad文档预览2](https://kkview.cn/img/preview/preview-cad-pdf.png)
考虑说明篇幅原因就不贴其他格式文件的预览效果了感兴趣的可以参考下面的实例搭建下
![cad文档预览2](./doc/img/preview/preview-cad-pdf.png)
#### 10. Excel文件纯前端渲染效果
![Excel文件纯前端渲染效果](https://kkview.cn/img/preview/preview-xlsx-web.png)
![Excel文件纯前端渲染效果](./doc/img/preview/preview-xlsx-web.png)
#### 11. 流程图bpmn文件预览效果
![流程图bpmn文件预览效果](https://kkview.cn/img/preview/preview-bpmn.png)
![流程图bpmn文件预览效果](./doc/img/preview/preview-bpmn.png)
#### 12. 3D模型文件预览效果
![3D模型文件预览效果](https://kkview.cn/img/preview/preview-3ds.png)
![3D模型文件预览效果](./doc/img/preview/preview-3ds.png)
#### 13. dcm医疗数位影像文件预览效果
![dcm医疗数位影像文件预览效果](https://kkview.cn/img/preview/preview-dcm.png)
![dcm医疗数位影像文件预览效果](./doc/img/preview/preview-dcm.png)
#### 14. drawio流程图预览效果
![dcdrawio流程图预览效果](https://kkview.cn/img/preview/preview-drawio.png)
![drawio流程图预览效果](./doc/img/preview/preview-drawio.png)
考虑说明篇幅原因就不贴其他格式文件的预览效果了感兴趣的可以参考下面的实例搭建下
### 快速开始
> 项目使用技术
@ -148,7 +149,56 @@ pdf预览模式预览效果如下
### 历史更新记录
#### > 2023年07月05v4.3 版本发布
#### > 2025年01月16v4.4.0 版本发布
### 新增功能
1. xlsx 新增支持打印功能
2. 配置文件新增启用 GZIP 压缩
3. CAD 格式新增支持转换成 SVG TIF 格式新增超时结束线程管理
4. 新增删除文件使用验证码校验
5. 新增 xbrl 格式预览支持
6. PDF 预览新增控制签名绘图插图控制搜索定位页码定义显示内容等功能
7. 新增 CSV 格式前端解析支持
8. 新增 ARM64 下的 Docker 镜像支持
9. 新增 Office 预览支持转换超时属性设置功能
10. 新增预览文件 host 黑名单机制
### 优化
1. 优化 OFD 移动端预览 页面不自适应
2. 更新 xlsx 前端解析组件加速解析速度
3. 升级 CAD 组件
4. office 功能调整支持批注转换页码限制生成水印等功能
5. 升级 markdown 组件
6. 升级 dcm 解析组件
7. 升级 PDF.JS 解析组件
8. 更换视频播放插件为 ckplayer
9. tif 解析更加智能化支持被修改的图片格式
10. 针对大小文本文件检测字符编码的正确率处理并发隐患
11. 重构下载文件的代码新增通用的文件服务器认证访问的设计
12. 更新 bootstrap 组件并精简掉不需要的文件
13. 更新 epub 版本优化 epub 显示效果
14. 解决定时清除缓存时对于多媒体类型文件只删除了磁盘缓存文件
15. 自动检测已安装 Office 组件增加 LibreOffice 7.5 & 7.6 版本默认路径
16. 修改 drawio 默认为预览模式
17. 新增 PDF 线程管理超时管理内存缓存管理更新 PDF 解析组件版本
18. 优化 Dockerfile支持真正的跨平台构建镜像
### 修复
1. 修复 forceUpdatedCache 属性设置但本地缓存文件不更新的问题
2. 修复 PDF 解密加密文件转换成功后后台报错的问题
3. 修复 BPMN 不支持跨域的问题
4. 修复压缩包二级反代特殊符号错误问题
5. 修复视频跨域配置导致视频无法预览的问题
6. 修复 TXT 文本类分页二次加载问题
7. 修复 Drawio 缺少 Base64 组件的问题
8. 修复 Markdown 被转义问题
9. 修复 EPUB 跨域报错问题
10. 修复 URL 特殊符号问题
11. 修复压缩包穿越漏洞
12. 修复压缩获取路径错误图片合集路径错误水印问题等 BUG
13. 修复前端解析 XLSX 包含 EMF 格式文件错误问题
#### > 2023年07月05日v4.3.0 版本发布
#### 新增功能:
1. 新增dcm等医疗数位影像预

170
SECURITY_CONFIG.md Normal file
View File

@ -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
---
**安全提示**定期检查和更新信任主机列表遵循最小权限原则

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -5,7 +5,7 @@ RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.aliyun.com@g' /etc/apt/sources.li
sed -i 's@//ports.ubuntu.com@//mirrors.aliyun.com@g' /etc/apt/sources.list.d/ubuntu.sources &&\
apt-get update &&\
export DEBIAN_FRONTEND=noninteractive &&\
apt-get install -y --no-install-recommends openjdk-8-jre tzdata locales xfonts-utils fontconfig libreoffice-nogui &&\
apt-get install -y --no-install-recommends openjdk-21-jre tzdata locales xfonts-utils fontconfig libreoffice-nogui &&\
echo 'Asia/Shanghai' > /etc/timezone &&\
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime &&\
localedef -i zh_CN -c -f UTF-8 -A /usr/share/locale/locale.alias zh_CN.UTF-8 &&\

View File

@ -9,14 +9,14 @@
<version>4.4.0</version>
<properties>
<java.version>1.8</java.version>
<java.version>21</java.version>
<jodconverter.version>4.4.6</jodconverter.version>
<spring.boot.version>2.4.2</spring.boot.version>
<spring.boot.version>3.5.6</spring.boot.version>
<poi.version>5.2.2</poi.version>
<xdocreport.version>1.0.6</xdocreport.version>
<xstream.version>1.4.20</xstream.version>
<junrar.version>7.5.5</junrar.version>
<redisson.version>3.2.0</redisson.version>
<redisson.version>3.22.0</redisson.version>
<sevenzipjbinding.version>16.02-2.01</sevenzipjbinding.version>
<jchardet.version>1.0</jchardet.version>
<antlr.version>2.7.7</antlr.version>
@ -43,6 +43,7 @@
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.release>${java.version}</maven.compiler.release>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View File

@ -44,21 +44,16 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- web end -->
<!-- poi start -->
@ -100,9 +95,8 @@
</dependency>
<!-- poi start -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpcomponents.version}</version>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<!-- rar5 的支持 和其他众多压缩支持 可参考 package net.sf.sevenzipjbinding.ArchiveFormat; -->
@ -182,6 +176,12 @@
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>${pdfbox.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
@ -327,6 +327,14 @@
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>

View File

@ -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}
#禁止上传类型

View File

@ -308,7 +308,8 @@ public class ConfigConstants {
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 class ConfigConstants {
return fileUploadDisable;
}
@Value("${file.upload.disable:false}")
@Value("${file.upload.disable:true}")
public void setFileUploadDisable(Boolean fileUploadDisable) {
setFileUploadDisableValue(fileUploadDisable);
}

View File

@ -5,7 +5,7 @@ import org.slf4j.Logger;
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;

View File

@ -50,26 +50,21 @@ public class RedissonConfig {
.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;
}

View File

@ -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<String, FileType> 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);

View File

@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory;
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 class FileConvertQueueTask {
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);
}

View File

@ -31,7 +31,7 @@ import org.springframework.util.CollectionUtils;
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 class FileHandlerService implements InitializingBean {
sb.append("<script src=\"excel/excel.header.js\" type=\"text/javascript\"></script>");
sb.append("<link rel=\"stylesheet\" href=\"excel/excel.css\">");
} 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 class FileHandlerService implements InitializingBean {
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进行转义

View File

@ -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";

View File

@ -15,8 +15,8 @@ import org.springframework.core.Ordered;
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;

View File

@ -7,7 +7,7 @@ import org.apache.commons.lang3.StringUtils;
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;

View File

@ -9,6 +9,8 @@ import cn.keking.utils.DownloadUtils;
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 @@ import static cn.keking.service.impl.OfficeFilePreviewImpl.getPreviewType;
@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 class CadFilePreviewImpl implements FilePreview {
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转换异常请联系管理员");

View File

@ -0,0 +1,95 @@
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 文件预览处理实现
*/
@Service
public class JsonFilePreviewImpl implements FilePreview {
private static final Logger LOGGER = LoggerFactory.getLogger(JsonFilePreviewImpl.class);
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();
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
String filePath = fileAttribute.getOriginFilePath();
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(fileName) || !ConfigConstants.isCacheEnabled()) {
ReturnResponse<String> 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) {
LOGGER.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);
}
}

View File

@ -11,6 +11,8 @@ import org.bytedeco.ffmpeg.global.avcodec;
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 @@ import java.io.File;
@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 class MediaFilePreviewImpl implements FilePreview {
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 @@ public class MediaFilePreviewImpl implements FilePreview {
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) { //关闭

View File

@ -9,11 +9,17 @@ import cn.keking.utils.DownloadUtils;
import cn.keking.utils.EncodingDetects;
import cn.keking.utils.KkFileUtils;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.web.util.HtmlUtils;
import java.io.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
@ -23,10 +29,12 @@ import java.nio.charset.StandardCharsets;
@Service
public class SimTextFilePreviewImpl implements FilePreview {
private static final Logger LOGGER = LoggerFactory.getLogger(SimTextFilePreviewImpl.class);
private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview;
public SimTextFilePreviewImpl(FileHandlerService fileHandlerService,OtherFilePreviewImpl otherFilePreview) {
public SimTextFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
this.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview;
}
@ -47,7 +55,7 @@ public class SimTextFilePreviewImpl implements FilePreview {
}
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 +65,9 @@ public class SimTextFilePreviewImpl implements FilePreview {
try {
fileData = HtmlUtils.htmlEscape(textData(filePath,fileName));
} catch (IOException e) {
e.printStackTrace();
LOGGER.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;
}

View File

@ -6,9 +6,9 @@ import cn.keking.model.ReturnResponse;
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;

View File

@ -4,6 +4,8 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.extractor.ExtractorFactory;
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
@ -18,6 +20,7 @@ import java.nio.file.Paths;
*/
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 class OfficeUtils {
try {
propStream.close();//关闭文件输入流
} catch (IOException e) {
e.printStackTrace();
logger.error("Failed to close input stream for file: {}", path, e);
}
}
}
@ -76,7 +79,7 @@ public class OfficeUtils {
try {
propStream.close();//关闭文件输入流
} catch (IOException e) {
e.printStackTrace();
logger.error("Failed to close input stream for file: {}", path, e);
}
}
}

View File

@ -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 @@ import java.util.regex.Pattern;
* 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 class RarUtils {
str = new String(getUTF8BytesFromGBKString(str), StandardCharsets.UTF_8);
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
logger.error("Failed to convert string encoding: {}", str, e);
}
}
return str;

View File

@ -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 @@ import java.net.URL;
*/
public class SimpleEncodingDetects {
private static final Logger logger = LoggerFactory.getLogger(SimpleEncodingDetects.class);
/**
* 得到文件的编码
* @param content 文件内容
@ -65,10 +70,10 @@ public class SimpleEncodingDetects {
} 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);
}
}

View File

@ -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 class WebUtils {
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 class WebUtils {
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 class WebUtils {
* 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");

View File

@ -19,9 +19,9 @@ import org.springframework.web.bind.annotation.RestController;
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;

View File

@ -11,9 +11,9 @@ import cn.keking.utils.WebUtils;
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.bind.annotation.ResponseBody;
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;

View File

@ -4,8 +4,8 @@ import cn.keking.config.ConfigConstants;
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;
/**

View File

@ -4,8 +4,8 @@ import cn.keking.config.ConfigConstants;
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;
/**

View File

@ -1,7 +1,7 @@
package cn.keking.web.filter;
import javax.servlet.*;
import jakarta.servlet.*;
import java.io.IOException;
/**

View File

@ -3,10 +3,10 @@ package cn.keking.web.filter;
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;

View File

@ -10,7 +10,7 @@ import org.springframework.core.io.ClassPathResource;
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 class TrustDirFilter implements Filter {
byte[] bytes = FileCopyUtils.copyToByteArray(classPathResource.getInputStream());
this.notTrustDirView = new String(bytes, StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
logger.error("加载notTrustDir.html失败", e);
}
}

View File

@ -5,14 +5,16 @@ import cn.keking.utils.WebUtils;
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 @@ import org.springframework.util.FileCopyUtils;
*/
public class TrustHostFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(TrustHostFilter.class);
private String notTrustHostHtmlView;
@Override
@ -32,7 +35,7 @@ public class TrustHostFilter implements Filter {
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 class TrustHostFilter implements Filter {
}
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

View File

@ -2,9 +2,9 @@ package cn.keking.web.filter;
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;
/**

View File

@ -6,6 +6,7 @@
| < | < | | | | | | | __/ \ / | | | __/ \ V V /
|_|\_\ |_|\_\ |_| |_| |_| \___| \/ |_| \___| \_/\_/
=> Java Version :: ${java.version}
=> Spring Boot :: ${spring-boot.version}
=> kkFileView :: 4.4.0
=> Home site :: https://kkview.cn

View File

@ -8,7 +8,19 @@
*/
function initWaterMark() {
let watermarkTxt = '${watermarkTxt}';
if (watermarkTxt !== '') {
if (watermarkTxt === '') {
return;
}
let lastWidth = 0;
let lastHeight = 0;
const checkResize = () => {
const currentWidth = document.documentElement.scrollWidth;
const currentHeight = document.documentElement.scrollHeight;
// 检测尺寸是否变化
if (currentWidth === lastWidth && currentHeight === lastHeight) {
return;
}
// 如果变化了, 重新初始化水印
watermark.init({
watermark_txt: '${watermarkTxt}',
watermark_x: 0,
@ -25,7 +37,11 @@
watermark_height: ${watermarkHeight},
watermark_angle: ${watermarkAngle},
});
}
// 更新存储的宽口大小
lastWidth = currentWidth;
lastHeight = currentHeight;
};
setInterval(checkResize, 1000);
}
</script>

View File

@ -0,0 +1,384 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0">
<title>JSON文件预览</title>
<#include "*/commonHeader.ftl">
<script src="js/jquery-3.6.1.min.js" type="text/javascript"></script>
<link rel="stylesheet" href="bootstrap/css/bootstrap.min.css"/>
<script src="bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
<script src="js/base64.min.js" type="text/javascript"></script>
<style>
body {
font-family: 'Courier New', Courier, monospace;
}
.container {
max-width: 100%;
padding: 20px;
}
#json {
padding: 0;
overflow-x: auto;
}
pre {
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;
font-weight: bold;
}
.json-string {
color: #1A1AA6;
}
.json-number {
color: #1C00CF;
}
.json-boolean {
color: #0D22FF;
font-weight: bold;
}
.json-null {
color: #808080;
font-weight: bold;
}
.json-toggle {
cursor: pointer;
color: #666;
user-select: none;
display: inline-block;
width: 16px;
font-weight: bold;
}
.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;
}
</style>
</head>
<body>
<input hidden id="textData" value="${textData}"/>
<div class="container">
<div class="panel panel-default">
<div id="formatted_btn" class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapseOne">
${file.name}
</a>
</h4>
</div>
<div id="raw_btn" class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapseOne">
${file.name}
</a>
</h4>
</div>
<div id="json" class="panel-body">
</div>
</div>
</div>
<script>
/**
* 初始化
*/
window.onload = function () {
$("#formatted_btn").hide();
initWaterMark();
loadJsonData();
}
/**
* HTML 反转义(用于还原后端转义的内容)
* 使用浏览器的 DOM 来正确解码所有 HTML 实体
*/
function htmlUnescape(str) {
if (!str || str.length === 0) return "";
var textarea = document.createElement('textarea');
textarea.innerHTML = str;
return textarea.value;
}
/**
* HTML 转义(用于安全显示)
*/
function htmlEscape(str) {
if (!str || str.length === 0) return "";
var s = str;
s = s.replace(/&/g, "&amp;");
s = s.replace(/</g, "&lt;");
s = s.replace(/>/g, "&gt;");
s = s.replace(/"/g, "&quot;");
s = s.replace(/'/g, "&#39;");
return s;
}
/**
* 移除 BOM (Byte Order Mark)
*/
function removeBOM(str) {
if (str.charCodeAt(0) === 0xFEFF) {
return str.substring(1);
}
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 '<span class="json-null">null</span>';
}
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 '<span class="json-string">"' + htmlEscape(escapedStr) + '"</span>';
} else if (typeof obj === 'number') {
return '<span class="json-number">' + obj + '</span>';
} else if (typeof obj === 'boolean') {
return '<span class="json-boolean">' + obj + '</span>';
}
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 += '<span class="line-number">' + lineNumber++ + '</span>';
}
html += '<span class="json-toggle" onclick="toggleJsonNode(\'' + nodeId + '\')">▼</span> ';
html += openBracket + '\n';
html += '<div id="' + nodeId + '" class="json-node">';
for (var i = 0; i < entries.length; i++) {
var key = isArray ? i : entries[i];
var value = isArray ? entries[i] : obj[entries[i]];
html += '<span class="line-number">' + lineNumber++ + '</span>';
html += indentStr + ' ';
if (!isArray) {
html += '<span class="json-key">"' + htmlEscape(key) + '"</span>: ';
}
// 如果值是对象或数组,跳过它的行号(因为已经在上面添加了)
html += buildJsonTree(value, indent + 1, true);
if (i < entries.length - 1) {
html += ',';
}
html += '\n';
}
html += '</div>';
html += '<span class="line-number">' + lineNumber++ + '</span>';
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 = '▼';
});
}
/**
* 全部收起
*/
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
var cls = 'json-number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'json-key';
} else {
cls = 'json-string';
}
} else if (/true|false/.test(match)) {
cls = 'json-boolean';
} else if (/null/.test(match)) {
cls = 'json-null';
}
return '<span class="' + cls + '">' + match + '</span>';
});
}
/**
* 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 = decodeBase64UTF8($("#textData").val());
// 1. 先反转义 HTML 实体(因为后端已经转义过)
textData = htmlUnescape(textData);
// 2. 移除 BOM
textData = removeBOM(textData);
// 保存原始文本(用于显示时再次转义以保证安全)
window.rawText = "<pre style='background-color: #FFFFFF; border: none; margin: 0;'>" + htmlEscape(textData) + "</pre>";
// 尝试解析并格式化 JSON
try {
var jsonObj = JSON.parse(textData);
// 重置行号计数器
lineNumber = 1;
// 构建树形视图
var treeHtml = '<div style="padding: 20px;">';
treeHtml += '<div style="margin-bottom: 10px;">';
treeHtml += '<button onclick="expandAll()" class="btn btn-sm btn-default" style="margin-right: 5px;">全部展开</button>';
treeHtml += '<button onclick="collapseAll()" class="btn btn-sm btn-default">全部收起</button>';
treeHtml += '</div>';
treeHtml += '<pre style="background-color: #f8f9fa; border: none; margin: 0;">';
treeHtml += buildJsonTree(jsonObj, 0);
treeHtml += '</pre></div>';
window.formattedJson = treeHtml;
// 默认显示树形视图
$("#json").html(window.formattedJson);
} catch (e) {
// 如果不是有效的 JSON显示错误并回退到原始文本
window.formattedJson = "<div class='alert alert-warning'>JSON 解析失败: " + htmlEscape(e.message) + "</div>" + window.rawText;
$("#json").html(window.formattedJson);
}
} catch (e) {
$("#json").html("<div class='alert alert-danger'>文件加载失败: " + htmlEscape(e.message) + "</div>");
}
}
/**
* 按钮点击事件
*/
$(function () {
$("#formatted_btn").click(function () {
$("#json").html(window.formattedJson);
$("#raw_btn").show();
$("#formatted_btn").hide();
});
$("#raw_btn").click(function () {
$("#json").html(window.rawText);
$("#formatted_btn").show();
$("#raw_btn").hide();
});
});
</script>
</body>
</html>

View File

@ -148,6 +148,18 @@
<input type="file" id="file" name="file" style="float: left; margin: 0 auto; font-size:22px;" placeholder="请选择文件"/>
<input type="button" id="fileUploadBtn" class="btn btn-success" value=" "/>
</form>
<#else>
<div style="padding: 20px; margin: 10px 0; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px;">
<p style="margin: 0; color: #6c757d; font-size: 16px;">
文件上传功能默认已禁用。如需开启,请通过以下方式配置:
<br/>
• 配置文件:<code>file.upload.disable=false</code>
<br/>
• 环境变量:<code>KK_FILE_UPLOAD_DISABLE=false</code>
<br/>
<strong style="color: #dc3545;">请注意:文件上传限开发环境调试使用,生产环境建议保持关闭状态,避免非法上传导致的安全隐患。</strong>
</p>
</div>
</#if>
<table id="table" data-pagination="true"></table>
</div>

View File

@ -21,12 +21,4 @@ public class WebUtilsTests {
String out = "https://file.keking.cn/demo/%23hello%26world.txt?param0=0&param1=1";
assert WebUtils.encodeUrlFileName(in).equals(out);
}
@Test
void encodeUrlFullFileNameTestWithParams() {
// 测试对URL中使用fullfilename参数的文件名部分进行UTF-8编码
String in = "https://file.keking.cn/demo/download?param0=0&fullfilename=hello#0.txt";
String out = "https://file.keking.cn/demo/download?param0=0&fullfilename=hello%230.txt";
assert WebUtils.encodeUrlFileName(in).equals(out);
}
}