当前位置: 首页 > news >正文

kkfile安全预览minio的文件

上一个项目使用了minio作为文件存储服务,搭配上kkfileview作为文件预览的组件,所以这篇博客作为轮子写出来发表,也算是为了自己以后方便。

​ 做的功能主要包括minio上传下载文件、bucket`管理、以及文件暴露地址预览和安全文件预览。

1.配置

​ 在application中引入二者相关的配置,其中miniofileDownloadPrefixkkfileviewpreviewTokenName是为了完成自己的需求添加的。

minio:endpoint: http://10.247.82.182:23000 #Minio服务所在地址bucketName: dyyj #存储桶名称accessKey: pansoft #访问的keysecretKey:  #访问的秘钥fileDownloadPrefix: http://0.0.0.0:80/prod-api# 文件预览软件访问路径
kkfileview:# 访问地址server: http://0.0.0.0:9091/# 单文件预览-路径拼接single: onlinePreview# 多图片预览-路径拼接images: picturesPreviewpreviewTokenName: file:preview_token- # 预览时生成的key存放在redis中previewExpiredTime: 300 # 生成的令牌有效期

​ 对应的有两个Bean配置

@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {private String endpoint;private String accessKey;private String secretKey;private String bucketName;private String fileDownloadPrefix;@Beanpublic MinioClient minioClient() {return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();}
}
@Component
@ConfigurationProperties(prefix = "kkfileview")
public class KkFileViewProperties {/*** 文件预览服务地址,例如:https://192.168.136.238:8012/kkfileview/*/private String server;/*** 单文件预览路径拼接,例如:onlinePreview*/private String single;/*** 多图片预览路径拼接,例如:picturesPreview*/private String images;/*** Redis 中预览令牌的键名前缀*/private String previewTokenName;/*** 预览令牌有效期(秒)*/private Integer previewExpiredTime;// ================== Getter / Setter ==================public String getServer() {return server;}public void setServer(String server) {this.server = server;}public String getSingle() {return single;}public void setSingle(String single) {this.single = single;}public String getImages() {return images;}public void setImages(String images) {this.images = images;}public String getPreviewTokenName() {return previewTokenName;}public void setPreviewTokenName(String previewTokenName) {this.previewTokenName = previewTokenName;}public Integer getPreviewExpiredTime() {return previewExpiredTime;}public void setPreviewExpiredTime(Integer previewExpiredTime) {this.previewExpiredTime = previewExpiredTime;}// ================== 可选:便捷方法 ==================/*** 获取完整的单文件预览 URL(例如  https://192.168.136.238:8012/kkfileview/onlinePreview )*/public String getSinglePreviewUrl() {return appendPath(server, single);}/*** 获取完整的多图片预览 URL(例如  https://192.168.136.238:8012/kkfileview/picturesPreview )*/public String getImagesPreviewUrl() {return appendPath(server, images);}private String appendPath(String base, String path) {if (base == null) return path;if (!base.endsWith("/")) base = base + "/";return base + (path != null ? path : "");}
}

2.工具类

​ 工具类有的是从网上找的,有的是我自己写的,基本上比较全了;第一个工具类是用于进行minio的文件操作,第二个工具类用于实现隐藏minio地址实现文件单次有效的预览和下载。

​ 这里说下实现安全下载或者预览的思路(不过我们公司比较拉国企内网不太在乎这个):

​ 我们知道kkfile预览的urlhttps://file.kkview.cn/onlinePreview?url=base64之后的文件地址,说白了就是后边的东西要被读取成文件流,而minio的文件我们访问是http://地址:端口/bucket名称/文件名称,为了保证这个真实的地址不被暴露我选择是通过自己的后端应用去下载文件,也就是把这个真实url作为参数传递给一个后端的下载接口,变相的获得文件流进而预览。

​ 为了保证只被下载一次,所以每次下载之前都让前端调用接口获得一个token,进入下载接口之后验证这个token有效期之后立即删除,这样就能保证这个下载地址仅一次生效,我是将下载接口设计成/download?token=token&filename=这样的格式,细节就不讲了。

@Component
@Slf4j
public class MinioUtil {@Autowiredprivate MinioConfig prop;@Resourceprivate MinioClient minioClient;/*** 查看存储bucket是否存在* @return boolean*/public Boolean bucketExists(String bucketName) {Boolean found;try {found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());} catch (Exception e) {e.printStackTrace();return false;}return found;}/*** 创建存储bucket* @return Boolean*/public Boolean makeBucket(String bucketName) {try {minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());} catch (Exception e) {e.printStackTrace();return false;}return true;}/*** 删除存储bucket* @return Boolean*/public Boolean removeBucket(String bucketName) {try {minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());} catch (Exception e) {e.printStackTrace();return false;}return true;}/*** 获取全部bucket*/public List<Bucket> getAllBuckets() {try {List<Bucket> buckets = minioClient.listBuckets();return buckets;} catch (Exception e) {e.printStackTrace();}return null;}/*** 文件上传** @param file 文件* @return Boolean*/public String upload(MultipartFile file, String foldName) {String originalFilename = file.getOriginalFilename();if (StringUtils.isBlank(originalFilename)){throw new RuntimeException();}String fileName = UUID.fastUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));String objectName = "";if (StringUtils.isNotEmpty(foldName)) {objectName = foldName +"/"+ DateUtil.date().toString("yyyy-MM-dd") + "/" + fileName;} else {objectName = DateUtil.date().toString("yyyy-MM-dd") + "/" + fileName;}try {PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(prop.getBucketName()).object(objectName).stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build();//文件名称相同会覆盖minioClient.putObject(objectArgs);} catch (Exception e) {e.printStackTrace();return null;}return objectName;}/*** 预览图片* @param fileName* @return*/public String preview(String fileName){// 查看文件地址GetPresignedObjectUrlArgs build = new GetPresignedObjectUrlArgs().builder().bucket(prop.getBucketName()).object(fileName).method(io.minio.http.Method.GET).build();try {String url = minioClient.getPresignedObjectUrl(build);return url;} catch (Exception e) {e.printStackTrace();}return null;}/*** 文件下载* @param fileName 文件名称* @param res response* @return Boolean*/public void download(String filename, HttpServletResponse response) throws ServiceException {try {InputStream inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(prop.getBucketName()).object(filename).build());// 设置响应头信息,告诉前端浏览器下载文件response.setHeader("Content-Disposition", "inline; filename=" + URLEncoder.encode(filename, "UTF-8"));response.setContentType("application/octet-stream");// 获取输出流进行写入数据OutputStream outputStream = response.getOutputStream();// 将输入流复制到输出流byte[] buffer = new byte[4096];int bytesRead = -1;while ((bytesRead = inputStream.read(buffer)) != -1) {outputStream.write(buffer, 0, bytesRead);}// 关闭流资源inputStream.close();outputStream.close();} catch (Exception e) {log.error("文件下载失败:" + e.getMessage());throw new ServiceException("文件下载失败");}}/*** 查看文件对象* @return 存储bucket内文件对象信息*/public List<Item> listObjects() {Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(prop.getBucketName()).build());List<Item> items = new ArrayList<>();try {for (Result<Item> result : results) {items.add(result.get());}} catch (Exception e) {e.printStackTrace();return null;}return items;}/*** 删除* @param fileName* @return* @throws Exception*/public boolean remove(String fileName){try {minioClient.removeObject( RemoveObjectArgs.builder().bucket(prop.getBucketName()).object(fileName).build());}catch (Exception e){return false;}return true;}public List<String> urlList() throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(prop.getBucketName()).recursive(true).build());List<String> urlList = new ArrayList<>();for (Result<Item> result : results) {Item item = result.get();urlList.add(item.objectName());  // 这里就是完整的路径,例如 folder1/file1.txt}return urlList;}}
/*** @author qihaotian* @description: 用于提供文件预览时的服务* @date 2025/11/3 16:11*/
@Component
public class FilePreviewUtil {@Resourceprivate KkFileViewProperties kkfile;@Resourceprivate RedisCache redisService;/*** 发放令牌 - 生成一个临时、短效的访问令牌* @param filename filename MinIO 中存储的实际文件名* @return 生成的临时 token 字符串*/public String generateToken(String filename) {// 1. 生成一个唯一的 UUID 作为 tokenString token = UUID.randomUUID().toString().replaceAll("-", "");// 2. 存储 token 到 Redis,value 存入文件名,并设置过期时间// 存储格式:key = file:preview_token:UUID, value = filenameredisService.setCacheObject(kkfile.getPreviewTokenName() + token,filename,kkfile.getPreviewExpiredTime(),TimeUnit.SECONDS);// 3. 返回 tokenreturn token;}/*** 鉴权令牌 验证 token 是否合法、是否过期,并获取文件名* @param token token URL 路径中传递的临时 token* @return 对应的 MinIO 文件名 (如果验证通过)* @throws ServiceException 如果 token 无效或已过期*/public String validateToken(String token) throws ServiceException {if (StringUtils.isBlank(token)) {throw new ServiceException("文件访问令牌不能为空");}// 1. 从 Redis 中获取 token 对应的文件名String filename = redisService.getCacheObject(kkfile.getPreviewTokenName() + token);if (StringUtils.isBlank(filename)) {// token 不存在(未生成过)或已过期/被手动清除throw new ServiceException("文件访问令牌无效或已过期");}// 2. 【核心安全机制】验证通过后,立即删除 token,实现单次访问 (One-Time-Token)// 这样 KKFile 请求完文件流后,该 token 立即失效,防止被二次使用或恶意分享。redisService.deleteObject(kkfile.getPreviewTokenName() + token);// 3. 验证成功,返回文件名return filename;}
}

3.接口和服务

​ 接口按需取得就行,之前还想写个批量操作的没有写完好像。

@Slf4j
@RestController
@RequestMapping("/minio")
public class FileController {@Autowiredprivate MinioUtil minioUtil;@Autowiredprivate MinioConfig prop;@Resourceprivate FilePreviewUtil filePreviewUtil;@Resourceprivate KkFileViewProperties kkfile;@Resourceprivate final KkFileViewProperties kkFileViewProperties;public FileController(KkFileViewProperties kkFileViewProperties) {this.kkFileViewProperties = kkFileViewProperties;}@ApiOperation(value = "查看存储bucket是否存在")@GetMapping("/bucketExists")public AjaxResult bucketExists(@RequestParam("bucketName") String bucketName) {return AjaxResult.success().put("bucketName",minioUtil.bucketExists(bucketName));}@ApiOperation(value = "创建存储bucket")@GetMapping("/makeBucket")public AjaxResult makeBucket(String bucketName) {return AjaxResult.success().put("bucketName",minioUtil.makeBucket(bucketName));}@ApiOperation(value = "删除存储bucket")@GetMapping("/removeBucket")public AjaxResult removeBucket(String bucketName) {return AjaxResult.success().put("bucketName",minioUtil.removeBucket(bucketName));}@ApiOperation(value = "获取全部bucket")@GetMapping("/getAllBuckets")public AjaxResult getAllBuckets() {List<Bucket> allBuckets = minioUtil.getAllBuckets();return AjaxResult.success().put("allBuckets",allBuckets);}@ApiOperation(value = "文件上传返回url和文件名")@PostMapping("/upload")public AjaxResult upload(@RequestParam("file") MultipartFile file, @RequestParam(required = false) String foldName) {String originalFilename = file.getOriginalFilename();String objectName = minioUtil.upload(file, foldName);if (null != objectName) {return AjaxResult.success().put("url",(prop.getEndpoint() + "/" + prop.getBucketName() + "/" + objectName)).put("originalName",originalFilename);}return AjaxResult.error("上传文件失败!");}//    @ApiOperation(value = "图片/视频预览")
//    @GetMapping("/preview")
//    public AjaxResult preview(@RequestParam("fileName") String fileName) {
//        return AjaxResult.success().put("fileName",minioUtil.preview(fileName));
//    }//    @ApiOperation(value = "文件下载")
//    @GetMapping("/download")
//    public AjaxResult download(@RequestParam("fileName") String fileName, HttpServletResponse res) {
//        minioUtil.download(fileName,res);
//        return AjaxResult.success();
//    }@ApiOperation(value = "删除文件", notes = "根据url地址删除文件")@PostMapping("/delete")public AjaxResult remove(String url) {String objName = url.substring(url.lastIndexOf(prop.getBucketName()+"/") + prop.getBucketName().length()+1);minioUtil.remove(objName);return AjaxResult.success().put("objName",objName);}@ApiOperation(value = "文件列表", notes = "获取文件列表")@GetMapping("/list-file")public AjaxResult listFile() throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {List<String> list = minioUtil.urlList(); // 循环调用remove就行return AjaxResult.success();}@ApiOperation("获取预览文件的key")@GetMapping("/get-preview-key")public AjaxResult getPreviewKey(@RequestParam("filename") String filename) {return AjaxResult.success().put("previewKey",filePreviewUtil.generateToken(filename));}@ApiOperation("文件下载")@GetMapping("/download")public void getDownload(@RequestParam("token") String token,@RequestParam("filename") String filename,HttpServletResponse response) {try {String actualFilename = filePreviewUtil.validateToken(token);minioUtil.download(actualFilename, response);} catch (ServiceException e) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 Unauthorized 或 403 Forbidden}}@ApiOperation("kkfile文件预览(不暴露minio地址,只能预览一次)")@GetMapping("/preview")public String getPreviewUrl(@RequestParam("token") String token, @RequestParam("filename") String filename) {// 参数非空校验if (StringUtils.isEmpty(token)) {throw new ServiceException("token不能为空");}if (StringUtils.isEmpty(filename)) {throw new ServiceException("filename不能为空");}// 从后往前截取到第一个 "." 作为文件拓展名String filenameExtension = filename.substring(filename.lastIndexOf(".") + 1);// 示例 http://192.168.136.238:9876/ice/test/2025-11-13/1909ab9e-6863-427f-a742-df49d12e2905.pdf&fullfilename=2025-11-13.pdfString downloadPath = prop.getFileDownloadPrefix() + "filename=" + filename + "&token=" + token + "&fullfilename="+ LocalDate.now() + "." + filenameExtension;String kkfilePreviewUrl = kkfile.getServer()  + kkfile.getSingle() + "?url=";return  kkfilePreviewUrl + Base64.encode(downloadPath) + "&officePreviewType=" + filenameExtension;}@ApiOperation("文件预览(允许暴露地址)")@GetMapping("/preview-expose")public String getPreviewUrlExpose(@RequestParam("filename") String filename) throws UnsupportedEncodingException {// 示例 http://192.168.136.238:9876/ice/test/2025-11-13/1909ab9e-6863-427f-a742-df49d12e2905.pdfString downloadPath = prop.getEndpoint() + "/" + prop.getBucketName() + filename;String kkfilePreviewUrl = kkfile.getServer()  + kkfile.getSingle() + "?url=";// 转化下载地址成kkfile能够识别的return  kkfilePreviewUrl + Base64.encodeUrlSafe(downloadPath);}
}
http://www.gsyq.cn/news/1456055.html

相关文章:

  • 图论入门:从基础到遍历算法
  • 免费高效的跨语言语义工具:cross-en-de-fr-roberta-sentence-transformer安装与配置指南
  • 小型运油船价格多少 - 舒雯文化
  • Python中模块导入方式
  • Logback 1.5.34 发布:修复反序列化漏洞,增强异常处理能力
  • 2026婚纱摄影行业白皮书:丽江影楼合规标杆与市场真相 - GrowthUME
  • Haon-Chen/e5-omni-7B完全安装指南:从Sentence Transformers到多模态环境配置
  • Linux 内核中的 epoll:从 syscall 底层原理到高并发架构启示
  • Adobe-GenP 3.0终极指南:免费激活Adobe CC全系列软件
  • 2026-2027年度在线浊度计十大国产品牌综合实力排行榜与技术选型白皮书 - 水质仪表品牌排行榜
  • 当AI安全告警准确率跌破61.3%——独家复盘某云厂商误报风暴事件(含混淆矩阵调优SOP与阈值动态算法)
  • AI 推广公司哪家好?优推宝摘金 AI 凭 GEO 技术给出答案 - 新闻快传
  • Unity手游热更新调试实战:VSCode + EmmyLua 连接真机Player全流程
  • 2026年便携式浊度计十大品牌权威排行:精准选型、稳定运行与全场景适配指南 - 水质仪表品牌排行榜
  • cann/cannbot-skills 大型PR检视场景
  • 【AI Daily】AI日报 2026-06-02
  • jsdiff:如何用JavaScript实现专业级文本差异比对?[特殊字符]
  • 通达信缠论插件:3分钟实现自动笔段中枢分析的终极解决方案
  • 龙岩新罗区承宥工程担保:福建全场景合规保函服务提供商 - 奔跑123
  • 好用还专业!盘点2026年口碑爆棚的AI论文写作工具
  • AI架构的转变:从向量到图谱
  • 从CHI 2016看人机交互的感知革命:触觉重定向、预触摸与概率编程
  • 真正替人干脏活累活!华盛顿大学推出JobBench,最强AI只拿45.9
  • 从10美元鼠标到macOS生产力利器的技术蜕变:Mac Mouse Fix深度解析
  • 为什么Palmer Penguins是数据科学入门的最佳选择:终极指南
  • 2026 AI自动化采集实战:如何用 Claude Code 进行网络爬虫?
  • 2026 潍坊卫生间漏水维修免踩坑指南,靠谱的防水补漏公司权威推荐:卫生间、阳台、屋顶、地下室、飘窗、外墙漏水,专业防水公司TOP5口碑榜+全维度测评(2026年6月最新深度行业资讯) - 防水资讯
  • 2026 泉州卫生间漏水维修免踩坑指南,靠谱的防水补漏公司权威推荐:卫生间、阳台、屋顶、地下室、飘窗、外墙漏水,专业防水公司TOP5口碑榜+全维度测评(2026年6月最新深度行业资讯) - 防水资讯
  • 重复内容渲染优化:从计算复用到图像空间与场景描述双路径实践
  • 2026 沧州卫生间漏水维修免踩坑指南,靠谱的防水补漏公司权威推荐:卫生间、阳台、屋顶、地下室、飘窗、外墙漏水,专业防水公司TOP5口碑榜+全维度测评(2026年6月最新深度行业资讯) - 防水资讯