Skip to content

v2.1.0(20260909) ​

IMPORTANT

v2.1.0 同时调整 Java 运行基线、Sa-Token 登录态、资源访问契约和前后端文件接口,不能按普通补丁版本直接覆盖。存量系统应安排维护窗口,前后端成套发布,并让所有用户重新登录。

1. 主要变化 ​

  • Java 构建与运行基线由 21 升级为 25。
  • Spring Boot 从 4.1.0 升级到 4.1.1,Sa-Token 从 1.45.0 升级到 1.46.0。
  • 登录态公共类迁移到 sz-common-security,包名调整为 com.sz.security.core.model。
  • Docker 镜像统一使用 Java 25,并改为 Spring Boot 分层构建。
  • 删除任意 URL 代理下载,资源访问统一为 DIRECT / PRESIGNED / PROTECTED。
  • 受保护文件通过业务固定下载/预览接口访问;不存在 ticket、token、bizType 或通用授权器流程。
  • 代码生成器和前端组件同步支持 ResourceRef[]、资源场景及业务下载 API。
  • 新增后端 HTML 内容净化器,前端同步加强富文本净化和上传校验。

本版本没有数据库结构变更,但包含资源场景 YAML 调整和一个仅用于官方演示数据的 Liquibase 数据 changeSet。

2. 升级前准备 ​

升级前请完成:

  1. 备份数据库、Redis、config/{profile}、前端 .env* 和部署脚本。
  2. 记录当前 JAR、前端静态资源和镜像,确保可以回滚。
  3. 检查二开代码是否引用旧登录态包名、旧 SVG 插件、旧容器 JAR 路径、/common/files/download 或 serve-mode: TOKEN。
  4. 盘点所有保存文件 JSON 的业务字段,确认每个受保护文件都能关联到活动的 sys_resource.id。
  5. 升级期间停止全部 v2.0.2 后端节点,不要让 Sa-Token 1.45 与 1.46 节点共同使用同一份登录态缓存。

3. Java、登录态与部署升级 ​

3.1 升级到 JDK 25 ​

v2.1.0 后开始使用 Java 25 字节码,开发、CI 和生产环境都必须使用 JDK 25。升级后确认:

bash
java -version
mvn -version

两条命令都应显示 Java 25。再按目标数据库执行完整验证:

bash
mvn clean verify -DskipTests=false -DskipITs=false -Pmysql
mvn clean verify -DskipTests=false -DskipITs=false -Ppostgresql

3.2 清理旧登录态缓存 ​

Sa-Token 1.46 增加 JSON 类型白名单,旧登录态缓存可能无法正常反序列化。升级时必须清理 Sa-Token 登录态并让用户重新登录。

3.3 修改旧包名 ​

旧包名新包名
com.sz.core.common.entity.BaseUserInfocom.sz.security.core.model.BaseUserInfo
com.sz.core.common.entity.LoginUsercom.sz.security.core.model.LoginUser
com.sz.core.common.entity.RoleMenuScopeVOcom.sz.security.core.model.RoleMenuScopeVO

3.4 检查 Docker 部署 ​

官方镜像统一使用 Java 25,默认标签与 -jdk25 标签指向同一镜像。镜像内 JAR 路径从 /app.jar 调整为:

text
/application/application.jar

/config、/logs、/data 挂载目录保持不变。自定义 Dockerfile、Compose、Kubernetes、健康检查或诊断脚本如硬编码旧路径,需要同步修改。

4. 资源访问安全升级 ​

旧 URL 代理下载接口存在服务端请求伪造(SSRF)漏洞。在默认鉴权配置下,利用该问题需要有效登录态,并非未登录即可利用,也不以管理员权限为前提。已登录用户可能使服务端访问非预期地址,实际影响取决于部署环境的网络可达范围。登录限制缩小了利用范围,但不能替代修复。

v2.1.0 删除该接口,改用业务权限校验和已登记资源读取;包含旧接口的二开项目也需要同步改造,不能只升级前端或隐藏下载按钮。

WARNING

上述登录前提基于项目默认鉴权配置。若曾将旧接口加入免登录白名单或以其他方式放开匿名访问,则不适用此限制,应立即禁用该接口,再按下文完成迁移。

4.1 统一访问规则 ​

serve-mode典型用途accessUrl实际访问方式
DIRECT头像、Logo、公开富文本图片稳定公开地址浏览器直接访问
PRESIGNED私有 OSS 中无需业务权限的短时展示短时签名地址浏览器直连存储
PROTECTED教师附件、模板当前/历史等业务文件null调用所属业务的下载/预览接口

省略 serve-mode 时默认 PROTECTED。公开资源必须显式配置 DIRECT;PRESIGNED 只解决存储层短时访问,不能替代按钮权限、数据范围或业务记录关系校验。

PROTECTED 不使用 ticket 或临时 token,也不需要填写 bizType/bizCode。业务含义由稳定的场景 code/name 表达,授权规则写在实际业务 Service 中。

4.2 配置迁移 ​

旧的 serve-mode: TOKEN 已删除,必须改为 PROTECTED。受保护场景不能配置供浏览器直接访问的 base-url。

yaml
sz:
  resource:
    root: ./data
    default-storage-type: LOCAL
    scenes:
      - code: admin.user.logo
        name: 账户头像
        type: LOCAL
        serve-mode: DIRECT
        path: logo
        base-url: /api/admin/resource/file/logo
        naming: ORIGINAL
        path-strategy: BIZ_DATE
        exts: [svg, png, jpg, jpeg, webp, gif]
        max-size: 3

      - code: template.excel
        name: 模板文件管理 / 导入模板
        type: LOCAL
        serve-mode: PROTECTED
        path: template
        naming: ORIGINAL
        path-strategy: DATE
        exts: [xls, xlsx]
        max-size: 10

      - code: teacher.attachment
        name: 教师统计 / 附件
        type: LOCAL
        serve-mode: PROTECTED
        path: teacher-attachments
        naming: ORIGINAL
        path-strategy: BIZ_DATE
        exts: [png, jpg, jpeg, webp, gif, pdf, doc, docx, xls, xlsx]
        max-size: 10

      - code: system.protected
        name: 通用业务附件
        type: LOCAL
        serve-mode: PROTECTED
        path: protected
        naming: ORIGINAL
        path-strategy: DATE
        exts: [png, jpg, jpeg, webp, gif, pdf, doc, docx, xls, xlsx]
        max-size: 10

system.temp 仍是 DIRECT 兼容场景;新生成的普通业务附件默认使用 system.protected。生产环境的 DIRECT base-url 必须由浏览器可访问的 Nginx、OSS 或受控资源服务承载,不能保留浏览器自身无法访问的 127.0.0.1 地址。

4.3 ResourceRef 与保存规则 ​

受管多文件字段统一保存 ResourceRef[]:

ts
export type ResourceRef = {
  resourceId: string;
  sceneCode?: string;
  objectKey?: string;
  originName?: string;
  contentType?: string;
  accessUrl?: string | null;
};

升级后遵循以下约束:

  • 前端始终把 resourceId 当字符串,不使用 number、parseInt() 或算术运算;
  • sys_resource 是受管资源元数据的权威来源;
  • accessUrl 是只读派生字段,只供 DIRECT/PRESIGNED 展示,不入库、不参与授权;
  • create/update 不能原样保存客户端提交的 sceneCode/objectKey/originName/contentType;
  • 后端只接收 resourceId 作为引用标识,再从活动的 sys_resource 重建稳定元数据;
  • 新增引用必须由当前用户上传,已有引用在更新时可以保留;
  • scene 必须与业务字段配置一致,重复 resourceId 会按提交顺序去重;
  • A/B 更新为 A/C 后只保存 A/C,解除 B 的业务引用不会自动物理删除文件。

4.4 受保护业务接口 ​

已内置三组固定接口:

业务下载接口预览接口校验
教师统计POST /api/admin/teacher-statistics/{id}/resources/{resourceId}/downloadPOST /api/admin/teacher-statistics/{id}/resources/{resourceId}/preview查询权限、数据范围、记录包含资源
当前模板POST /api/admin/sys-temp-file/{id}/resources/{resourceId}/downloadPOST /api/admin/sys-temp-file/{id}/resources/{resourceId}/preview查询权限、活动记录、双字段一致
模板历史POST /api/admin/sys-temp-file-history/{id}/resources/{resourceId}/downloadPOST /api/admin/sys-temp-file-history/{id}/resources/{resourceId}/preview查询权限、活动父记录、历史记录双字段一致

业务接口先完成权限和记录关系校验,再调用 ResourceDownloadService.download(resourceId) 或 preview(resourceId)。公共服务只负责从活动的 sys_resource 读取存储流和生成安全响应,不判断具体业务权限。

新业务的标准结构:

java
@SaCheckPermission("contract.record.query_table")
@PostMapping("/{id}/resources/{resourceId}/download")
public ResponseEntity<StreamingResponseBody> downloadResource(
        @PathVariable Long id, @PathVariable Long resourceId) {
    contractService.validateResourceAccess(id, resourceId);
    return resourceDownloadService.download(resourceId);
}

validateResourceAccess 必须复用业务既有的数据范围查询,确认当前记录确实引用 resourceId,并加入“仅已发布可下载”等业务状态规则。禁止创建跳过业务校验的通用 resourceId 下载 Controller。

下载默认返回 attachment;预览仅允许 PNG、JPEG、WebP、GIF 和 PDF。HTML、SVG、Office 等主动或非白名单类型应改用下载,不允许 inline。

4.5 删除旧 URL 代理 ​

以下旧链路已经删除:

  • POST /api/admin/common/files/download;
  • ProxyDownloadDTO;
  • CommonService.urlDownload() 与实现;
  • 前端 useUrlDownload、fileDownload({ url })。

原 /common/download/templates classpath 模板下载接口仍保留,不属于任意 URL 代理。任何回滚方案都不能恢复由服务端请求调用方 URL 的能力。

迁移时按以下顺序处理:

  1. 搜索前后端及二开模块中的旧接口、urlDownload 和 URL 代理封装,移除调用及复制出来的同类实现。
  2. 受保护附件改为传递业务记录 ID 和 resourceId,由所属业务接口校验权限及引用关系后读取文件;公开资源使用场景返回的 accessUrl。
  3. 同步发布前后端,并确认所有后端节点已替换旧制品。不要为兼容旧前端重新提供接收任意 URL 的代理接口。

WARNING

暂时无法升级时,应在后端或反向代理禁用旧下载接口,并覆盖全部入口,包括可绕过代理直连的后端端口。本文路径使用默认 /api/admin 前缀,自定义部署应按实际路由封禁。此操作会使依赖旧接口的下载功能不可用,但仅隐藏按钮、要求登录或只封禁其中一个入口均不能替代修复。限制服务端出站网络可作为补充防护,不能替代删除旧接口。

4.6 历史数据与 Liquibase ​

本版本没有新增表、字段、索引或约束。sys_resource 表沿用已有结构。

demo/2.1.0/001_demo_resource_references.xml 是数据 changeSet,只在启用 demo changelog 时生效:

  • 为 8 个官方教师演示附件登记 sys_resource;
  • 为未被二开修改的教师统计 ID 25、26 补齐 resourceId;
  • 将当前模板和模板历史的演示 JSON 对齐到已有模板资源;
  • 使用精确原值摘要和前置条件,数据已被修改时 MARK_RAN,不覆盖用户数据;
  • 不提供自动 rollback,也不扫描或猜测自定义业务数据。

自定义历史文件必须逐条确认业务 JSON 和 sys_resource 的关系。能够确定映射时可编写项目自己的数据迁移;无法确定时应重新上传并保存业务记录。系统没有 OFF/AUDIT/REPAIR 迁移器,也没有 H01-H20 报告或自动修复开关。

4.7 头像字段 ​

头像继续使用 DIRECT,不改成 ResourceRef。数据库和提交字段保持 objectKey:

  • SysUserVO.logo / SysUserVO.logoUrl;
  • UserProfileVO.avatar / UserProfileVO.avatarUrl;
  • 专用登录返回 VO 中保留展示 URL;
  • BaseUserInfo 只保留 logo,不新增 URL 字段。

保存用户或个人资料时不得把 logoUrl/avatarUrl 回写数据库。

5. 代码生成器与前端接入 ​

5.1 代码生成器 ​

当字段使用 List<ResourceRef> 且控件为 fileUpload/imageUpload 时,生成器会生成:

  1. create/update 的资源引用规范化;
  2. Service 的 validateResourceAccess(id, resourceId);
  3. Controller 的固定下载/预览接口;
  4. 前端下载/预览 API;
  5. 附件列表的 FileDownloadList 绑定 bizId + downloadApi + previewApi;编辑表单的 UploadFiles 只绑定 bizId + downloadApi,没有预览按钮,也没有 previewApi 参数。

生成前应在字段配置中选择真实资源场景。system.protected 适合作为通用默认值;正式业务建议使用含义稳定的场景,例如 contract.attachment,并在各环境 oss.yml 中配置相同 code/name。

头像、Logo、公开富文本等 DIRECT/PRESIGNED 字段不应生成受保护业务接口。

5.2 前端组件 ​

FileDownloadList 的受保护用法:

vue
<FileDownloadList
  :files="row.attachments"
  :biz-id="String(row.id)"
  :download-api="downloadTeacherStatisticsResourceApi"
  :preview-api="previewTeacherStatisticsResourceApi"
/>

访问顺序:

  1. 有 bizId + 业务 API + resourceId 时调用业务接口并处理 Blob;
  2. 没有业务接口,但 DIRECT/PRESIGNED 返回 accessUrl 时直接访问;
  3. 两者都没有时拒绝访问,并提示从已保存的业务记录进入;
  4. 不回退到 URL 代理。

新增记录尚无业务 ID 时,新上传图片使用本地 Blob URL 预览;重新打开已保存记录后走业务接口。提交前应使用 normalizeResourceFiles() 去除本地临时字段、过滤上传中项目并按 resourceId 去重。

5.3 文件管理 ​

文件管理列表展示文件名、用途、大小、访问方式、存储方式、创建人和登记时间:

  • DIRECT:允许直接访问或复制地址;
  • PRESIGNED:按需生成短时地址;
  • PROTECTED:只看详情,明确提示从所属业务记录预览或下载;
  • system.protected:显示“通用业务附件”,不会虚构具体业务归属。

文件管理是资源登记和排障入口,不是绕过业务权限的统一下载中心。

6. HTML 安全净化与前端依赖 ​

后端新增 HtmlContentSanitizer,用于过滤富文本、CMS 和外部 HTML 中的脚本、事件属性、危险 URL、iframe 等内容。它不会自动处理所有字符串字段;二开业务应在富文本入库前主动调用,并回归正常排版、图片、链接和表格。

前端主要变化:

  • Jodit 4.12.26 → 4.13.23;
  • DOMPurify 3.4.12 → 3.4.14;
  • 富文本上传校验后端 code=0000 和 data 数组;
  • 富文本图片只直接支持 JPG、JPEG、PNG、GIF、WebP;
  • SvgIcon 改用 import.meta.glob,移除 vite-plugin-svg-icons。
bash
corepack prepare pnpm@10.17.1 --activate
pnpm install --frozen-lockfile
pnpm run type-check
pnpm run build

7. 升级后检查与回滚 ​

发布后应对实际部署制品完成以下检查,而不是只确认源码已经修改:

检查项预期结果
部署版本所有后端节点和前端静态资源均已更新;记录版本、提交号和镜像摘要,不仅依赖可变的 latest 标签
旧下载接口在有效登录态下确认旧路由已不存在或被明确禁用;未登录请求得到 401 只能说明鉴权生效,不能证明接口已移除
正常业务访问有权限的用户可从所属业务记录下载文件;允许预览的文件正常展示,其余类型使用下载
权限与资源关系未登录、无接口权限、超出数据范围,或资源不属于指定业务记录时,不返回文件内容
公开与受保护资源DIRECT 资源可按预期访问;PROTECTED 不返回公开地址,也不能通过旧静态映射、Nginx 或 OSS 公开地址绕过业务接口
历史数据存量附件、当前模板及模板历史均能关联活动的 sys_resource 记录;自定义数据不能依赖官方演示 changeSet 自动修复
登录与保存旧会话失效后可重新登录,头像和附件保存正常;展示 URL 不回写业务数据

回归时使用测试账号和自有测试记录,无需在生产环境运行漏洞利用脚本。升级后检查用于确认部署和访问规则生效,不能用单个接口响应替代完整的安全评估。

如需回滚,应同步考虑前后端契约、登录态和已迁移数据。旧制品可能重新带回漏洞,必须持续禁用旧 URL 代理接口;数据库和业务 JSON 按升级前备份及实际迁移记录恢复,不能假设演示数据 changeSet 提供自动回滚。

返回升级索引