基于java實(shí)現(xiàn)具有時(shí)效性文件鏈接
1.寫在前面
之前在某個(gè)項(xiàng)目中,用戶上傳的文件(頭像、視頻、文檔等等)是通過(guò)靜態(tài)路徑來(lái)訪問(wèn)的,這導(dǎo)致一旦該文件的路徑暴露,用戶可以在不登錄的情況下,直接訪問(wèn)服務(wù)器的文件資源??蛻粢虼颂岢觯募穆窂奖仨氁哂袝r(shí)效性(類似對(duì)象存儲(chǔ)的文件鏈接,超過(guò)一定時(shí)間就無(wú)法訪問(wèn))。
我希望最終可以像對(duì)象存儲(chǔ)一樣,文件鏈接可以設(shè)定訪問(wèn)時(shí)間,超期后直接報(bào)錯(cuò)。比較常見(jiàn)的方法可以通過(guò)緩存來(lái)實(shí)現(xiàn)。思路如下:
1.后端在接收到文件的訪問(wèn)請(qǐng)求時(shí),生成一個(gè)唯一的文件ID,將它和指定的前綴拼接為URL返還給前端,并同時(shí)將此ID作為key,文件信息作為value存入緩存信息。
2.前端通過(guò)返回的文件鏈接訪問(wèn)后端,后端對(duì)鏈接中的ID進(jìn)行截取,前往緩存中查詢,如果存在則以流的的形式返回文件,否則直接返回錯(cuò)誤信息。
時(shí)序圖如下:
2.時(shí)序圖
3.關(guān)鍵技術(shù)點(diǎn)
3.1時(shí)效性
對(duì)于時(shí)效性,我們可以通過(guò)緩存來(lái)實(shí)現(xiàn)。通過(guò)給緩存添加時(shí)間限制,到期就移除,從而達(dá)到URL的時(shí)效性。這里我們通過(guò)簡(jiǎn)單的緩存工具類來(lái)實(shí)現(xiàn)(也可用redis,數(shù)據(jù)庫(kù)理論上也可以,通過(guò)存文件的存入時(shí)間,每次文件請(qǐng)求的時(shí)候判斷一下是否超期,思路上是可以的,但是考慮訪問(wèn)速度并不推薦)。
以下是緩存對(duì)象:
@AllArgsConstructor public class CacheEntry<V> { //存儲(chǔ)數(shù)據(jù) private final V value; //過(guò)期時(shí)間 private final long expirationTimeMillis; /** * 是否過(guò)期 * @return 布爾值 */ public boolean isExpired() { return System.currentTimeMillis() > expirationTimeMillis; } public V getValue() { return value; } }
再寫個(gè)簡(jiǎn)單的工具類來(lái)操作:
@Component public class CacheManagerUtil<K,V> { private final Map<K, CacheEntry<V>> cacheMap; private final ScheduledExecutorService scheduler; /** * 默認(rèn)過(guò)期時(shí)間 3小時(shí) */ public static long TTL=1000*60*60*3L; public CacheManagerUtil() { cacheMap = new ConcurrentHashMap<>(); scheduler = Executors.newScheduledThreadPool(1); } /** * 存值 * @param key 鍵 * @param value 值 * @param expirationTimeMillis 過(guò)期時(shí)間 */ public void put(K key, V value, long expirationTimeMillis) { expirationTimeMillis += System.currentTimeMillis(); CacheEntry<V> entry = new CacheEntry<>(value, expirationTimeMillis); cacheMap.put(key, entry); // 定時(shí)任務(wù),在過(guò)期時(shí)間后自動(dòng)銷毀緩存條目 scheduler.schedule(() -> cacheMap.remove(key), expirationTimeMillis, TimeUnit.MILLISECONDS); } /** * 根據(jù)鍵取值 * @param key 鍵 * @return 值 */ public V get(K key){ CacheEntry<V> entry = cacheMap.get(key); if (entry != null && !entry.isExpired()) { return entry.getValue(); } return null; } /** * 根據(jù)建刪除對(duì)應(yīng)的鍵值對(duì) * @param key 鍵 */ public void remove(K key) { cacheMap.remove(key); } /** * 獲取緩存鍵列表 * @return 緩存鍵列表 */ public List<K> getKeys(){ return new ArrayList<>(cacheMap.keySet()); } }
3.2同一個(gè)接口返回文件資源和錯(cuò)誤信息
首先我們要知道,瀏覽器在訪問(wèn)某個(gè)鏈接的時(shí)候,會(huì)根據(jù)服務(wù)器返回的Response的中的Content-type來(lái)決定如何執(zhí)行響應(yīng)。如果服務(wù)器未指定任何Content-type,瀏覽器有內(nèi)置的處理能力來(lái)對(duì)常見(jiàn)的鏈接(圖片、PDF)進(jìn)行處理,常見(jiàn)的比如說(shuō)使用瀏覽器去打開(kāi)圖片,打開(kāi)控制臺(tái),瀏覽器會(huì)有一個(gè)默認(rèn)的網(wǎng)頁(yè)將文件鏈接放在里面。
因此,我們可以通過(guò)同一個(gè)Mapping,實(shí)現(xiàn)不同的Response的返回,從而達(dá)到既能返回文件,又可以返回錯(cuò)誤信息。需要的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格式錯(cuò)誤返回
response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8");
這里可以使用策略模式進(jìn)行改寫,首先我們構(gòu)建一個(gè)簡(jiǎn)單的處理響應(yīng)的接口:
public interface ResponseStrategy { /** * 處理響應(yīng) * @param response 響應(yīng) * @param fileCacheVO 文件緩存VO * @return 操作結(jié)果 */ void handleResponse(HttpServletResponse response, FileCacheVO fileCacheVO); }
A.下載文件策略處理類
@Slf4j @Component public class DownLoadFileStrategy implements ResponseStrategy { /** * 最大字節(jié)大小 */ private static final int MAX_BYTE_SIZE = 4096; @Override public void handleResponse(HttpServletResponse response, FileCacheVO fileCacheVO) { //設(shè)置響應(yīng)頭 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.無(wú)文件策略類
@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文件上傳接口
文件上傳我們可以分為三步:文件接收并存儲(chǔ)、數(shù)據(jù)庫(kù)記錄插入和存入緩存。具體代碼如下:
/** * 上傳文件 * * @param uploadFile 上傳文件 * @return 返回臨時(shí)鏈接 */ @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)); }
這里我們通過(guò)fileService同時(shí)進(jìn)行文件的存儲(chǔ)和數(shù)據(jù)庫(kù)插入,然后通過(guò)緩存工具類cacheManagerUtil存儲(chǔ),使用Apifox進(jìn)行接口測(cè)試,結(jié)果如下:
其中返回的文件鏈接為:<http://127.0.0.1:8080/timelyFileLink/file/22bb8cda-99d8-4b0b-a6aa-9e3d516b5a5f>
URL中的22bb8cda-99d8-4b0b-a6aa-9e3d516b5a5f就是我們緩存到服務(wù)器上的key
4.2鏈接校驗(yàn)
這里我們分為兩步:緩存校驗(yàn)和返回不同的Response。代碼如下:
/** * 文件下載 * * @param uuid 文件唯一ID * @param response 響應(yīng) */ @GetMapping("/file/{uuid}") public void file(@Validated @PathVariable @NotBlank(message = "文件ID不能為空") String uuid, HttpServletResponse response) { //校驗(yàn) FileCacheVO fileCacheVO = cacheManagerUtil.get(uuid); ResponseStrategy strategy = fileCacheVO == null ? new NoFileStrategy() : new DownLoadFileStrategy(); strategy.handleResponse(response, fileCacheVO); }
我們先使用緩存工具類進(jìn)行校驗(yàn),如果文件是超期或者不存在的,瀏覽器會(huì)直接返回錯(cuò)誤的json信息:
如果文件是存在,瀏覽器輸入這個(gè)鏈接會(huì)直接下載:
如果html中有地方調(diào)用了這個(gè)圖片,那么圖片會(huì)自動(dòng)反顯,這里我們寫一個(gè)簡(jiǎn)單的html測(cè)試代碼:
<html> <img src="http://127.0.0.1:8080/timelyFileLink/file/22bb8cda-99d8-4b0b-a6aa-9e3d516b5a5f"> </html>
頁(yè)面如下:
可以看到對(duì)于流式的接口,瀏覽器會(huì)自動(dòng)反顯圖片。
5.總結(jié)
整個(gè)Demo都是基于緩存操作來(lái)實(shí)現(xiàn)鏈接的時(shí)效性,對(duì)于需要展示文件鏈接的頁(yè)面,可以通過(guò)這種時(shí)效性鏈接來(lái)實(shí)現(xiàn)訪問(wèn)文件的安全性。但由于是操作緩存,實(shí)際我們?cè)谑褂玫臅r(shí)候需要考慮用戶數(shù)量、接口頻率、緩存大小等等問(wèn)題,如果是正式項(xiàng)目,我其實(shí)更建議使用redis,方便進(jìn)行緩存的管理以及問(wèn)題的排查。
以上就是基于java實(shí)現(xiàn)具有時(shí)效性文件鏈接的詳細(xì)內(nèi)容,更多關(guān)于java時(shí)效性文件鏈接的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java配置HTTP/Socks代理的簡(jiǎn)單快速上手方法
這篇文章主要為大家介紹了Java配置HTTP/Socks代理的簡(jiǎn)單快速上手方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08synchronized及JUC顯式locks?使用原理解析
這篇文章主要為大家介紹了synchronized及JUC顯式locks?使用原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12使用mybatis-plus-generator進(jìn)行代碼自動(dòng)生成的方法
這篇文章主要介紹了使用mybatis-plus-generator進(jìn)行代碼自動(dòng)生成的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06Spring?IOC?xml方式進(jìn)行工廠Bean操作詳解
這篇文章主要介紹了Spring?IOC?xml方式進(jìn)行工廠Bean操作,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2023-01-01spring boot配置MySQL數(shù)據(jù)庫(kù)連接、Hikari連接池和Mybatis的簡(jiǎn)單配置方法
這篇文章主要介紹了spring boot配置MySQL數(shù)據(jù)庫(kù)連接、Hikari連接池和Mybatis的簡(jiǎn)單配置方法,需要的朋友可以參考下2018-03-03Java中Arrays.sort自定義一維數(shù)組、二維數(shù)組的排序方式
這篇文章主要介紹了Java中Arrays.sort自定義一維數(shù)組、二維數(shù)組的排序方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08Java使用雙異步實(shí)現(xiàn)將Excel的數(shù)據(jù)導(dǎo)入數(shù)據(jù)庫(kù)
在開(kāi)發(fā)中,我們經(jīng)常會(huì)遇到這樣的需求,將Excel的數(shù)據(jù)導(dǎo)入數(shù)據(jù)庫(kù)中,這篇文章主要來(lái)和大家講講Java如何使用雙異步實(shí)現(xiàn)將Excel的數(shù)據(jù)導(dǎo)入數(shù)據(jù)庫(kù),感興趣的可以了解下2024-01-01