基于java實現(xiàn)具有時效性文件鏈接
1.寫在前面
之前在某個項目中,用戶上傳的文件(頭像、視頻、文檔等等)是通過靜態(tài)路徑來訪問的,這導(dǎo)致一旦該文件的路徑暴露,用戶可以在不登錄的情況下,直接訪問服務(wù)器的文件資源??蛻粢虼颂岢?,文件的路徑必須要具有時效性(類似對象存儲的文件鏈接,超過一定時間就無法訪問)。
我希望最終可以像對象存儲一樣,文件鏈接可以設(shè)定訪問時間,超期后直接報錯。比較常見的方法可以通過緩存來實現(xiàn)。思路如下:
1.后端在接收到文件的訪問請求時,生成一個唯一的文件ID,將它和指定的前綴拼接為URL返還給前端,并同時將此ID作為key,文件信息作為value存入緩存信息。
2.前端通過返回的文件鏈接訪問后端,后端對鏈接中的ID進行截取,前往緩存中查詢,如果存在則以流的的形式返回文件,否則直接返回錯誤信息。
時序圖如下:
2.時序圖

3.關(guān)鍵技術(shù)點
3.1時效性
對于時效性,我們可以通過緩存來實現(xiàn)。通過給緩存添加時間限制,到期就移除,從而達(dá)到URL的時效性。這里我們通過簡單的緩存工具類來實現(xiàn)(也可用redis,數(shù)據(jù)庫理論上也可以,通過存文件的存入時間,每次文件請求的時候判斷一下是否超期,思路上是可以的,但是考慮訪問速度并不推薦)。
以下是緩存對象:
@AllArgsConstructor
public class CacheEntry<V> {
//存儲數(shù)據(jù)
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;
/**
* 默認(rèn)過期時間 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);
// 定時任務(wù),在過期時間后自動銷毀緩存條目
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ù)建刪除對應(yīng)的鍵值對
* @param key 鍵
*/
public void remove(K key) {
cacheMap.remove(key);
}
/**
* 獲取緩存鍵列表
* @return 緩存鍵列表
*/
public List<K> getKeys(){
return new ArrayList<>(cacheMap.keySet());
}
}
3.2同一個接口返回文件資源和錯誤信息
首先我們要知道,瀏覽器在訪問某個鏈接的時候,會根據(jù)服務(wù)器返回的Response的中的Content-type來決定如何執(zhí)行響應(yīng)。如果服務(wù)器未指定任何Content-type,瀏覽器有內(nèi)置的處理能力來對常見的鏈接(圖片、PDF)進行處理,常見的比如說使用瀏覽器去打開圖片,打開控制臺,瀏覽器會有一個默認(rèn)的網(wǎng)頁將文件鏈接放在里面。
因此,我們可以通過同一個Mapping,實現(xiàn)不同的Response的返回,從而達(dá)到既能返回文件,又可以返回錯誤信息。需要的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");
這里可以使用策略模式進行改寫,首先我們構(gòu)建一個簡單的處理響應(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.無文件策略類
@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文件上傳接口
文件上傳我們可以分為三步:文件接收并存儲、數(shù)據(jù)庫記錄插入和存入緩存。具體代碼如下:
/**
* 上傳文件
*
* @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同時進行文件的存儲和數(shù)據(jù)庫插入,然后通過緩存工具類cacheManagerUtil存儲,使用Apifox進行接口測試,結(jié)果如下:

其中返回的文件鏈接為:<http://127.0.0.1:8080/timelyFileLink/file/22bb8cda-99d8-4b0b-a6aa-9e3d516b5a5f>URL中的22bb8cda-99d8-4b0b-a6aa-9e3d516b5a5f就是我們緩存到服務(wù)器上的key
4.2鏈接校驗
這里我們分為兩步:緩存校驗和返回不同的Response。代碼如下:
/**
* 文件下載
*
* @param uuid 文件唯一ID
* @param response 響應(yīng)
*/
@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信息:

如果文件是存在,瀏覽器輸入這個鏈接會直接下載:

如果html中有地方調(diào)用了這個圖片,那么圖片會自動反顯,這里我們寫一個簡單的html測試代碼:
<html>
<img src="http://127.0.0.1:8080/timelyFileLink/file/22bb8cda-99d8-4b0b-a6aa-9e3d516b5a5f">
</html>
頁面如下:

可以看到對于流式的接口,瀏覽器會自動反顯圖片。
5.總結(jié)
整個Demo都是基于緩存操作來實現(xiàn)鏈接的時效性,對于需要展示文件鏈接的頁面,可以通過這種時效性鏈接來實現(xiàn)訪問文件的安全性。但由于是操作緩存,實際我們在使用的時候需要考慮用戶數(shù)量、接口頻率、緩存大小等等問題,如果是正式項目,我其實更建議使用redis,方便進行緩存的管理以及問題的排查。
以上就是基于java實現(xiàn)具有時效性文件鏈接的詳細(xì)內(nèi)容,更多關(guān)于java時效性文件鏈接的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot實現(xiàn)調(diào)用百度ocr實現(xiàn)身份識別+二要素校驗功能
本文介紹了如何使用Spring Boot調(diào)用百度OCR服務(wù)進行身份識別,并通過二要素校驗確保信息準(zhǔn)確性,感興趣的朋友一起看看吧2025-03-03
基于Java實現(xiàn)PDF文本旋轉(zhuǎn)傾斜
這篇文章主要介紹了基于Java實現(xiàn)PDF文本旋轉(zhuǎn)傾斜,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-05-05
java生成json實現(xiàn)隱藏掉關(guān)鍵屬性
這篇文章主要介紹了java生成json實現(xiàn)隱藏掉關(guān)鍵屬性,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03
SpringSecurity中的UserDetailsService深度解析
文章介紹了Spring Security中的UserDetailsService接口,它用于自定義用戶認(rèn)證邏輯,通過實現(xiàn)該接口,可以自定義用戶認(rèn)證過程,文章詳細(xì)解釋了UserDetailsService接口的返回值、方法參數(shù)以及可能出現(xiàn)的異常,如UsernameNotFoundException,感興趣的朋友跟隨小編一起看看吧2025-11-11
Java向上轉(zhuǎn)型和向下轉(zhuǎn)型實例解析
這篇文章主要介紹了Java向上轉(zhuǎn)型和向下轉(zhuǎn)型實例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-02-02

