PHP-防止靜態資源被直接訪問
用PHP寫後端,想要達到用戶登錄後才可以訪問一些圖片和視頻資源的效果,因此要阻止用戶直接輸入資源地址訪問資源。
找了一些資料,自己總結了幾種方法。
1.根據Referer頭——防盜鏈
瀏覽器在發起HTTP請求時一般都會一同發送Referer頭。Referer頭是用戶跳轉前的頁面,也就是通過哪個頁面發起的請求。通過禁止非法Referer頭的資源請求可以一定程度防止資源被非法訪問。一般這個都是在web服務器軟件上設置而不是後端處理。由於這個方法並不是很靠譜所以沒試過(畢竟請求頭是由請求方控制的),不過應付一般用戶足夠了,特別適合用來防止其他網站掛自己服務器的資源鏈接以轉移服務器負載,不過一般小網站用不到就是了。
2.通過復雜文件名
給資源文件賦以隨機的文件名,用數據庫記錄,然後定期或不定期更新文件名,用戶訪問頁面時後端php動態的查詢資源文件名。
這個方法還算可以,缺點是頻繁的數據庫連接將會增大服務器負載。如果服務器支持可以試試數據庫持久連接,不過需要注意持久連接的一些坑,不然可能造成連接鎖死之類的問題。
3.隱藏資源——將資源文件放在用戶無法訪問的目錄。
這樣做有兩種方案可以選擇,一是在用戶需要訪問時將資源文件復制到相應位置(可通過創建硬鏈接避免時間和空間浪費),二是將所有對資源的訪問重定位到一個文件,後端統一驗證身份後輸出文件內容。第一種其實意義不大,因為總是要讓用戶訪問的,那只要有授權用戶在需要資源文件,你就得把資源放在那裡,然後就誰都可以訪問了,再然後發現問題回到方法2了——還是得改文件名。
第二種方法是比較靠譜的方法。比如我將資源訪問重定向到resource.php這個文件,然後驗證身份後根據GET請求參數去找用戶請求的文件,然後用readfile函數讀取並輸出文件就OK。用戶的看到的網頁源代碼將類似這樣:
<img src="http://127.0.0.1/resource.php?path=filename"/>
filename可以是真正的資源相對路徑,因為用戶反正是無法直接訪問的,暴露文件路徑反而可以省去查數據庫的消耗。需要注意的是,如果需要傳遞的文件路徑中包含特殊字符如“/”等需要轉義。可在後端統一由某個接口封裝,生成安全的資源鏈接,類似這樣:
<?php function getURL($path) { return 'http://127.0.0.1/resource.php?path=' . urlencode($path); } ?> <body> <img src="<?php echo getURL('picture/dog.jpg')?>"> </body>然後resource.php中驗證用戶是否已授權,如果是且資源訪問合法,readfile('/resource_path/' . $_GET['path']),結束。
這應該是目前最靠譜的辦法,不過它也有缺陷。如果請求的資源是視頻這種比較大的文件,瀏覽器會一直等待資源接收完畢才顯示後面的內容,而不是頁面加載完再以流媒體的方式加載視頻資源,因此這個方法無法用於大文件資源。同時,測試發現,css中的url()資源不支持這樣的方式,因此背景圖片之類的資源也無法使用這種方法。
4.大雜儈——結合兩種方法
非常不幸,我要做的東西正好需要請求大量視頻資源,因此採用方法3中的第二類發現瀏覽器一直轉加載視頻,後面的評論等板塊得等視頻下載完成才加載,完全不能忍。於是想了半天,採取了折中的辦法:對於小圖片、少量圖片、文本資源採用方法3第二方案,也就是資源訪問重定向到resource.php統一處理;對於大圖片、視頻資源、大量圖片和css中的資源,則通過臨時硬鏈接的方式。上面3中已經說了readfile()輸出的方法,下面說說硬鏈接的具體做法。
我們知道,php中有個session的概念,它為一個會話生成一個id,並在客戶端以cookie的形式保存,在服務器端創建一個唯一的session文件,用於保存與相關會話相關的數據,每次用戶請求中包含的cookie便告訴服務器當前會話的相關數據,比如是否已經登錄等。
這裡正利用了php的session機制。
我在網站目錄中創建了一個temp目錄,它包含一個空文件index.html以防止用戶直接訪問目錄看到目錄下的文件列表(也可在服務器端配置禁止對目錄的訪問),因此用戶可以訪問該目錄下的文件而無法得知它包含哪些子目錄或文件,這是前提。
當用戶頁面需要請求一個需要授權訪問的資源時,如果用戶是授權的(通過session機制判斷),後端生成相應資源一個非隨機的硬鏈接。它是這樣的一個硬鏈接:
- 它的父目錄是
temp/当前会话session ID/ - 它的文件名由它的相對路徑通過哈希算法生成,再加上文件後綴
例如,如果我的資源目錄(禁止用戶訪問)是/resource_path,存在資源/resource_path/video/dog.mp4。網站目錄是/(對於用戶),包含temp子目錄。那麼我的頁面應該這樣寫:
之前的getURL函數變成這樣:
function getURL($path, $flow = false) {
//非法资源
if(!file_exists('/resource_path/' . $path)) {
return '';
}
//小文件资源,采用资源重定向方案
if(!$flow) {
return '/resource.php?path=' . urlencode($path);
}
//获取后缀
preg_match('/\.[^\.\/]*$/', $path, $sufix);
$filename = '/temp/'. session_id() . '/' . md5($path) . $sufix[0];
//判断资源链接是否已创建,WWW_ROOT为网站根目录实际路径
if(file_exists(WWW_ROOT . $filename)) {
return $filename;
}
//创建temp/sessionID目录
else if(!file_exists(dirname(WWW_ROOT . $filename))) {
mkdir(dirname(WWW_ROOT . $filename));
}
//创建硬链接。注意Windows不支持PHP的link函数,看你服务器平台
if(stripos(PHP_OS, 'WIN') === false) {
link(HEVER_ROOT . $path, HEVER_ROOT . $filename);
}
else {
system('mklink /H "' . HEVER_ROOT . $filename . '" "' . HEVER_ROOT . $path . '"');
}
return HEVER_HOME . $filename;
}
當然,md5()的參數也可以另外的算,不一定是相對路徑。
這樣做的好處是,當用戶再次請求相同的資源時,只需確保相應的硬鏈接存在即可直接不管了,因此需要這個硬鏈接的文件名是非隨機的避免查詢數據庫。而且這樣多了一層session id阻隔,即使非法用戶知道了生成資源鏈接文件名的規則也無法訪問到資源,因為資源鏈接是在當前會話的session id目錄下的,除非他能猜到某個會話的session id再模擬發送cookie——還不如讓他猜用戶名和密碼呢。
當然這個方法也不是完美的,由於可能會生成大量的硬鏈接和session文件並且廢棄後不會消失,需要定期執行清理腳本來清除過期的鏈接和文件。我的解決方案是,當用戶登錄或登出時觸發一個腳本,它會清除超過1天未訪問過的session文件和對應的temp目錄中的同名名錄(其實不是同名,session文件還有sess_前綴)。這個靈感來自於wordpress的偽cron機制。
需要注意的是,Windows系統不支持php的link函數,因此得用windows的shell。還有一點,清理php session文件時,如果php.ini中未配置session.save_path,在php中用session_save_path()可能獲取不到php默認的session文件存放目錄,因此建議在文件首主動配置session目錄,使用session_save_path(string $path)函數。
注:以上方案均未經過嚴密的測試,請謹慎參考。