feat(file): 扩展文件上传响应信息并增强匿名文件访问安全控制
- 在FileUploadRespVO中增加configId和path字段,丰富文件上传返回信息 - 新增selectByConfigIdAndPath方法用于按配置ID和路径查询文件记录 - 在getFileContent服务中添加存在性校验,防止已删除文件被匿名访问 - 更新getFileContent接口注释为"获取文件内容(匿名)",明确使用场景 - 为图片文件添加缓存控制头,设置max-age为一天并使用ETag实现条件缓存 - 通过DigestUtil计算文件内容SHA256作为ETag值,优化CDN和网关层缓存命中
This commit is contained in:
@@ -51,7 +51,8 @@ public class FileController {
|
||||
byte[] content = IoUtil.readBytes(file.getInputStream());
|
||||
FileDO fileDO = fileService.createFile(content, file.getOriginalFilename(),
|
||||
uploadReqVO.getDirectory(), file.getContentType());
|
||||
return success(new FileUploadRespVO(String.valueOf(fileDO.getId()), fileDO.getUrl()));
|
||||
return success(new FileUploadRespVO(String.valueOf(fileDO.getId()),
|
||||
fileDO.getConfigId(), fileDO.getPath(), fileDO.getUrl()));
|
||||
}
|
||||
|
||||
@GetMapping("/presigned-url")
|
||||
@@ -115,8 +116,7 @@ public class FileController {
|
||||
|
||||
@GetMapping("/{configId}/get/**")
|
||||
@PermitAll
|
||||
|
||||
@Operation(summary = "下载文件")
|
||||
@Operation(summary = "获取文件内容(匿名)", description = "富文本 <img src> 等匿名场景使用:图片走 inline 内联渲染,其它类型走 attachment 下载")
|
||||
@Parameter(name = "configId", description = "配置编号", required = true)
|
||||
public void getFileContent(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
|
||||
@@ -14,6 +14,12 @@ public class FileUploadRespVO {
|
||||
@Schema(description = "文件编号,Long 以字符串返回,避免前端精度丢失", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "22")
|
||||
private Long configId;
|
||||
|
||||
@Schema(description = "文件相对路径,含目录与文件名", requiredMode = Schema.RequiredMode.REQUIRED, example = "20260514/avatar_1747273800000.png")
|
||||
private String path;
|
||||
|
||||
@Schema(description = "文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/rdms.jpg")
|
||||
private String url;
|
||||
|
||||
|
||||
@@ -41,4 +41,18 @@ public interface FileMapper extends BaseMapperX<FileDO> {
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 (configId, path) 查询文件记录
|
||||
*
|
||||
* 用于匿名图片代理接口 {@code GET /system/file/{configId}/get/**} 的存在性校验:
|
||||
* 当 infra_file 已被逻辑删除时,MyBatis-Plus 全局过滤会自动加上 deleted=0,
|
||||
* 这里查不到即视为已删除,由调用方返 404 给浏览器(裂图),避免外部探测到对象存储仍可读
|
||||
*/
|
||||
default FileDO selectByConfigIdAndPath(Long configId, String path) {
|
||||
return selectOne(new LambdaQueryWrapperX<FileDO>()
|
||||
.eq(FileDO::getConfigId, configId)
|
||||
.eq(FileDO::getPath, path)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.njcn.rdms.module.system.framework.file.core.utils;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.njcn.rdms.framework.common.util.http.HttpUtils;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.SneakyThrows;
|
||||
@@ -86,6 +87,10 @@ public class FileTypeUtils {
|
||||
if (isImage(mineType)) {
|
||||
// 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论
|
||||
response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename));
|
||||
// 图片匿名代理的主要消费方是富文本 <img src>,反复访问同一张图,下发缓存头让浏览器 max-age 内不再回源
|
||||
// ETag 用字节 sha256,让 CDN / 网关层有条件做 304 命中
|
||||
response.setHeader("Cache-Control", "public, max-age=86400");
|
||||
response.setHeader("ETag", "\"" + DigestUtil.sha256Hex(content) + "\"");
|
||||
} else {
|
||||
response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
|
||||
}
|
||||
|
||||
@@ -207,6 +207,12 @@ public class FileServiceImpl implements FileService {
|
||||
|
||||
@Override
|
||||
public byte[] getFileContent(Long configId, String path) throws Exception {
|
||||
// 软删 / 不存在的记录直接拒绝
|
||||
// 该方法被匿名图片代理 (FileController#getFileContent) 复用,必须先按 DB 校验存在性,
|
||||
// 否则历史路径在 infra_file 被逻辑删除后,外部仍能匿名拉到对象存储字节
|
||||
if (fileMapper.selectByConfigIdAndPath(configId, path) == null) {
|
||||
return null;
|
||||
}
|
||||
FileClient client = fileConfigService.getFileClient(configId);
|
||||
Assert.notNull(client, "客户端({}) 不能为空", configId);
|
||||
return client.getContent(path);
|
||||
|
||||
Reference in New Issue
Block a user