开发者

基于java实现具有时效性文件链接

开发者 https://www.devze.com 2023-12-04 10:21 出处:网络 作者: ThreeBody1998
目录1.写在前面2.时序图3.关键技术点3.1时效性3.2同一个接口返回文件资源和错误信息4.流程及源码分析4.1文件上传接口4.2链接校验5.总结1.写在前面
目录
  • 1.写在前面
  • 2.时序图
  • 3.关键技术点
    • 3.1时效性
    • 3.2同一个接口返回文件资源和错误信息
  • 4.流程及源码分析
    • 4.1文件上传接口
    • 4.2链接校验
  • 5.总结

    1.写在前面

    之前在某个项目中,用户上传的文件(头像、视频、文档等等)是通过静态路径来访问的,这导致一旦该文件的路径暴露,用户可以在不登录的情况下,直接访问服务器的文件资源。客户因此提出,文件的路径必须要具有时效性(类似对象存储的文件链接,超过一定时间就无法访问)。

    我希望最终可以像对象存储一样,文件链接可以设定访问时间,超期后直接报错。比较常见的方法可以通过缓存来实现。思路如下:

    1.后端在接收到文件的访问请求时,生成一个唯一的文件ID,将它和指定的前缀拼接为URL返还给前端,并同时将此ID作为key,文件信息作为sNfvXrfevalue存入缓存信息。

    2.前端通过返回的文件链接访问后端,后端对链接中的ID进行截取,前往缓存中查询,如果存在则以流的的形式返回文件,否则直接返回错误信息。

    时序图如下:

    2.时序图

    基于java实现具有时效性文件链接

    3.关键技术点

    3.1时效性

    对于时效性,我们可以通过缓存来实现。通过给缓存添加时间限制,到期就移除,从而达到URL的时效性。这里我们通过简单的缓存工具类来实现(也可用Redis,数据库理论上也可以,通过存文件的存入时间,每次文件请求的时候判断一下是否超期,思路上是可以的,但是考虑访问速度并不推荐)。

    以下是缓存对象:

    @AllArgsConstructor
    public class CacheEntry<V> {
        //存储数据
        private final V value;
        //过期时间
        private final long expirationTimeMillis;
    
        /**
         * 是否过期
         * @return  布尔值
         */
        public boolean isExpired() {
            return System.currentTimeMillis() > expirationTimeMillis;
        }
    
        public V getValue() {
            return value;
        }
    }
    

    再写个简单的工具类来操作:

    @Component
    public class CacheManagerUtil<K,V> {
        private final Map<K, CacheEntry<V>> cacheMap;
        private final ScheduledExecutorService scheduler;
        /**
         * 默认过期时间 3小时
         */
        public static long TTL=1000*60*60*3L;
    
        public CacheManagerUtil() {
            cacheMap = new ConcurrentHashMap<>();
            scheduler = Executors.newScheduledThreadPool(1);
        }
    
        /**
         * 存值
         * @param key   键
         * @param value 值
         * @param expirationTimeMillis  过期时间
         */
        public void put(K key, V value, long expirationTimeMillis) {
            expirationTimeMillis += System.currentTimeMillis();
            CacheEntry<V> entry = new CacheEntry<>(value, expirationTimeMillis);
            cacheMap.put(key, entry);
            // 定时任务,在过期时间后自动销毁缓存条目
            scheduler.schedule(() -> cacheMap.remove(key), expirationTimeMillis, TimeUnit.MILLISECONDS);
        }
    
        /**
         * 根据键取值
         * @param key   键
         * @return  值
         */
        public V get(K key){
            CacheEntry<V> entry = cacheMap.get(key);
            if (entry != null && !entry.isExpired()) {
                return entry.getValue();
            }
            return null;
        }
    
        /**
         * 根据建删除对应的键值对
         * @param key   键
         */
        public void remove(K key) {
            cacheMap.remove(key);
        }
    
        /**
         * 获取缓存键列表
         * @return  缓存键列表
         */
        public List<K> getKeys(){
            return new ArrayList<>(cacheMap.keySet());
        }
    }
    

    3.2同一个接口返回文件资源和错误信息

    首先我们要知道,浏览器在访问某个链接的时候,会根据服务器返回的Response的中的Content-type来决定如何执行响应。如果服务器未指定任何Content-type,浏览器有内置的处理能力来对常见的链接(图片、PDF)进行处理,常见的比如说使用浏览器去打开图片,打开控制台,浏览器会有一个默认的网页将文件链接放在里面。

    因此,我们可以通过同一个Mapping,实现不同的Response的返回,从而达到既能返回文件,又可以返回错误信息。需要的Response格式如下:

    1.流式文件下载

    response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileCacheVO.getFileName());
    response.setHeader("pragma", "no-cache");
    response.setHeader("cache-control", "no-cache");
    response.setHeader("expires", "0");
    

    2.json格式错误返回

    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    response.setCharacterEncoding("UTF-8");
    

    这里可以使用策略模式进行改写,首先我们构建一个简单的处理响应的接口:

    public interface ResponseStrategy {
        /**
         * 处理响应
         * @param response  响应
         * @param fileCacheVO   文件缓存VO
         * @return  操作结果
         */
        void handleResponse(HttpServletResponse response, FileCacheVO fileCacheVO);
    }
    

    A.下载文件策略处理类

    @Slf4j
    @Component
    public class DownLoadFileStrategy implements ResponseStrategy {
    
        /**
         * 最大字节大小
         */
        private static final int MAX_BYTE_SIZE = 4096;
    
        @Override
        public void handleResponse(HttpServletResponse response, FileCacheVO fileCacheVO) {
            //设置响应头
            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileCacheVO.getFileName());
            response.setHeader("pragma", "no-cache");
            response.setHeader("cache-control", "no-cache");
            response.setHeader("expires", "0");
            //以流的形式返回文件
            Path filePath=Paths.get(fileCacheVO.getFilePath());
            try {
                Resource resource = new UrlResource(filePath.toUri());
                InputStream inputStream = Objects.requireNonNull(resource).getInputStream();
                var outputStream = response.getOutputStream();
                byte[] buffer = new byte[MAX_BYTE_SIZE];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
                outputStream.close();
            } catch (MalformedURLException e) {
                log.error("文件下载失败{}", e.getMessage());
                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            } catch (IOException e) {
                log编程客栈.error("文件IO异常{}", e.getMessage());
                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            }
        }
    }
    

    B.无文件策略类

    @Slf4j
    public class NoFileStrategy implements ResponseStrategy {
        @Override
        public void handleResponse(HttpServletResponse response, FileCacheVO fileCacheVO) {
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding("UTF-8");
            String errorJson=new ResponseResult<String>().setHttpResultEnum(HttpResultEnum.SERVER_ERROR).setMsg("文件不存在").toJsonString().toString();
            try {
                response.getWriter().write(errorJson);
            } catch (IOException e) {
                log.error("文件下载失败{}",e.getMessage());
                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            }
        }
    }
    

    4.流程及源码分析

    4.1文件上传接口

    文件上传我们可以分为三步:文件接收并存储、数据库记录插入和存入缓存。具体代码如下:

    /**
    * 上传文件
    *
    * @param uploadFile 上传文件
    * @return 返回临时链接
    */
    @PostMapping("/uploadFile")
    @Transactional(rollbackFor = Exception.class)
    public ResponseResult<FileVO> uploadFile(@Validated @NotNull(message = "上传文件不能为空") MultipartFile uploadFile) {
    	ResponseResult<FileVO> respo编程客栈nseResult = new ResponseResult<>();
    	//文件上传
    	FileUploadResult fileUploadResult = fileService.uploadFile(uploadFile);
    	//加入缓存
    	FileCacheVO fileCacheVO = new FileCacheVO(fileUploadResult);
    	cacheManagerUtil.put(fileUploadResult.getFileId(), fileCacheVO, CacheManagerUtil.TTL);
    	return responseResult.setData(new FileVO(fileUploadResult));
    }
    

    这里我们通过fileService同时进行文件的存储和数据库插入,然后通过缓存工具类cacheManagerUtil存储,使用Apifox进行接口测试,结果如下:

    基于java实现具有时效性文件链接

    其中返回的文件链接为:

    <http://127.0.0.1:8080/timelyFileLink/file/22bb8cda-99d8-4b0b-a6aa-9e3d516b5a编程5f>URL中的22bb8cda-99d8-4b0b-a6aa-9e3d516b5a5f就是我们缓存到服务器上的key

    4.2链接校验

    这里我们分为两步:缓存校验和返回不同的Response。代码如下:

    /**
    * 文件下载
    *
    * @param uuid     文件唯一ID
    * @param response 响应
    */
    @GetMapping("/file/{uuid}")
    public void file(@Validated @PathVariable @NotBlank(message = "文件ID不能为空") String uuid, HttpServletRe编程客栈sponse response) {
        //校验
        FileCacheVO fileCacheVO = cacheManagerUtil.get(uuid);
        ResponseStrategy strategy = fileCacheVO == null ? new NoFileStrategy() : new DownLoadFileStrategy();
        strategy.handleResponse(response, fileCacheVO);
    }
    

    我们先使用缓存工具类进行校验,如果文件是超期或者不存在的,浏览器会直接返回错误的json信息:

    基于java实现具有时效性文件链接

    如果文件是存在,浏览器输入这个链接会直接下载:

    基于java实现具有时效性文件链接

    如果html中有地方调用了这个图片,那么图片会自动反显,这里我们写一个简单的html测试代码:

    <html>
        <img src="http://127.0.0.1:8080/timelyFileLink/file/22bb8cda-99d8-4b0b-a6aa-9e3d516b5a5f"> 
    </html>
    

    页面如下:

    基于java实现具有时效性文件链接

    可以看到对于流式的接口,浏览器会自动反显图片。

    5.总结

    整个Demo都是基于缓存操作来实现链接的时效性,对于需要展示文件链接的页面,可以通过这种时效性链接来实现访问文件的安全性。但由于是操作缓存,实际我们在使用的时候需要考虑用户数量、接口频率、缓存大小等等问题,如果是正式项目,我其实更建议使用redis,方便进行缓存的管理以及问题的排查。

    以上就是基于Java实现具有时效性文件链接的详细内容,更多关于java时效性文件链接的资料请关注编程客栈(www.devze.com)其它相关文章!

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消