时效性文件链接实现思路

1.写在前面

        之前在某个项目中,用户上传的文件(头像、视频、文档等等)是通过静态路径来访问的,这导致一旦该文件的路径暴露,用户可以在不登录的情况下,直接访问服务器的文件资源。客户因此提出,文件的路径必须要具有时效性(类似对象存储的文件链接,超过一定时间就无法访问)。
        我希望最终可以像对象存储一样,文件链接可以设定访问时间,超期后直接报错。比较常见的方法可以通过缓存来实现。思路如下:
        1.后端在接收到文件的访问请求时,生成一个唯一的文件ID,将它和指定的前缀拼接为URL返还给前端,并同时将此ID作为key,文件信息作为value存入缓存信息。
        2.前端通过返回的文件链接访问后端,后端对链接中的ID进行截取,前往缓存中查询,如果存在则以流的的形式返回文件,否则直接返回错误信息。
        时序图如下:

2.时序图

时效性链接时序图.png

3.关键技术点

3.1时效性

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

 

java

复制代码

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

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

 

java

复制代码

@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.流式文件下载

 

java

复制代码

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格式错误返回

 

vbscript

复制代码

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

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

 

java

复制代码

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

        A.下载文件策略处理类

 

java

复制代码

@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.无文件策略类

 

java

复制代码

@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文件上传接口

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

 

java

复制代码

/** * 上传文件 * * @param uploadFile 上传文件 * @return 返回临时链接 */ @PostMapping("/uploadFile") @Transactional(rollbackFor = Exception.class) public ResponseResult<FileVO> uploadFile(@Validated @NotNull(message = "上传文件不能为空") MultipartFile uploadFile) { ResponseResult<FileVO> responseResult = 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进行接口测试,结果如下:


       其中返回的文件链接为:
<http://127.0.0.1:8080/timelyFileLink/file/22bb8cda-99d8-4b0b-a6aa-9e3d516b5a5f>
URL中的22bb8cda-99d8-4b0b-a6aa-9e3d516b5a5f就是我们缓存到服务器上的key

4.2链接校验

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

 

java

复制代码

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

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

image.png


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

image.png


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

 

html

复制代码

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

       页面如下:
 

image.png


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

5.总结

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

  • 18
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
cookie的时效性是指存储在浏览器中的cookie数据的有效时间。根据引用[3]的描述,cookie的时效性可以通过设定一个时间节点来确定。如果设置了具体的时效性,当超过这个时间节点时,浏览器会自动删除cookie中存储的数据。如果没有设定时效性,默认的时效性是会话时间。所谓的会话时间是指从浏览器打开执行前端程序的时间开始,到页面关闭时,cookie会被删除。所以,cookie的时效性要么是会话时间,要么是超过当前时间的时效性,而不可能是小于当前时间的时效性,因为小于当前时间的cookie会被删除并不存在了。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [Cookie的作用和使用](https://blog.csdn.net/u013041882/article/details/46889131)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [cookie的本地存储](https://blog.csdn.net/DcTbnk/article/details/105680753)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值