繞開 referrer 防盜鏈 以及服務器nodejs 作防盜鏈圖片中轉

1繞開 referrer 防盜鏈

最近處理了一個與referer有關的需求,發現裏面還是有一點門道的。因此在本篇文章整理了referer相關知識點,主要涉及圖片防盜鏈與如何繞開防盜鏈限制。

參考:
Referer-MDN

使用referer
Referer是HTTP請求頭的一個字段,包含了當前請求頁面的來源頁面的地址,通過該字段,我們可以檢測訪客是從哪裏來的。

那麼,referer到底有啥作用呢?

交互優化
在某些web應用的交互中,右上角會提供一個返回按鈕,方便用戶返回上一頁

其實現一般也比較簡單


複製代碼
這種處理方式隱藏的一個問題是:如果用戶從其他入口如分享鏈接等地方直接進來時,點擊這個按鈕是無法返回。

因此在點擊按鈕時,我們可以判斷document.referrer是否存在來優化交互:如果存在,則返回上一頁;如果不存在,則直接返回首頁。

應該注意到上面寫的是referer,而在DOM中,使用的是referrer,這是因此請求頭中的referer是由於歷史原因導致的拼寫錯誤,而在DOM規範中進行了修正,因此導致當前拼法並不統一的問題~

防盜鏈
當用戶訪問網頁時,referer就是前一個網頁的URL;如果是圖片的話,通常指的就是圖片所在的網頁。當瀏覽器向服務器發送請求時,referer就自動攜帶在HTTP請求頭了。

一個HTML頁面往往包含多種資源,這些資源通過標籤如script、img、link等形式嵌套在HTML文檔中,一個完整頁面往往需要經過發送多條HTTP請求下載資源,然後才能正常展示。由於HTML本身並沒有對嵌套資源的來源做限制,基於這樣的機制,盜鏈就成爲了一種手段。

下面是關於盜鏈的百度百科定義

盜鏈是指服務提供商自己不提供服務的內容,通過技術手段繞過其它有利益的最終用戶界面(如廣告),直接在自己的網站上向最終用戶提供其它服務提供商的服務內容,騙取最終用戶的瀏覽和點擊率。受益者不提供資源或提供很少的資源,而真正的服務提供商卻得不到任何的收益。

打個比方:A網站將自己的靜態資源如圖片或視頻等存放在服務器上。B網站在未經A允許的情況下,使用A網站的圖片或視頻資源,放置到自己的網站中。由於服務器資源是需要花錢的,這樣網站B盜取了網站A的空間和流量,而A沒有獲取任何利益卻承擔了資源使用費。B盜用A資源放到自己網站的行爲即爲盜鏈。

防盜鏈一般由下面幾種方式

定期修改文件名稱或路徑
通過referer,限制資源引用頁的來源
通過cookie、session等進行身份認證
圖片加水印等
這裏我們主要關注一下referer的防盜鏈的原理。下面是nginx的防盜鏈配置

location ~* .(gif|jpg|png)$ {
valid_referers none blocked *.phptest.com;
if ($invalid_referer) {
return 403;
}
}
複製代碼
這種方法是在server或者location段中加入:valid_referers。這個指令在referer頭的基礎上爲 $invalid_referer 變量賦值,其值爲0或1。如果valid_referers列表中沒有Referer頭的值, $invalid_referer將被設置爲1。

該指令支持none和blocked,

其中none表示空的來路,也就是直接訪問,比如直接在瀏覽器打開一個文件,
blocked表示被防火牆標記過的來路,*…com表示所有子域名。
通過referer,我們可以判斷請求的來源,從而決定服務器是否正常返回請求資源,達到控制請求的目的。

需要注意的是,在某些情況下,即使用戶是正常訪問網頁或圖片,也是不會攜帶referer的,比如直接在瀏覽器地址欄直接輸入資源URL,或通過瀏覽器新窗口打開頁面等。這種訪問是正常的,如果強制現在某些白名單referer名單才能訪問資源,則可能誤傷這一部分正常用戶,這也是爲什麼有的防盜鏈檢測中允許Referer頭部爲空通過檢測的情況。

既然如此,如果把referer隱藏掉,也可以繞開部分站點防盜鏈的限制,下面讓我們來看看如何實現隱藏referer的功能。

隱藏referer
參考

html禁用referer
以Referer方案寫一個圖片防盜鏈服務並實現網頁端"破解"
在利用部分站點防盜鏈限制允許referer爲空,或者我們僅僅是不想讓服務器知道訪問來源時,我們可以隱藏referer。

referrerPolicy
之前瀏覽器在請求資源時,會按自己的默認規則來決定是否加上Referrer。後來W3C發佈了Referrer-Policy草案,運行開發者靈活地控制自己網站的referer策略。主要包含下面策略

no-referrer:任何情況下都不發送 Referrer 信息;
no-referrer-when-downgrade (默認值):在沒有指定任何策略的情況下瀏覽器的默認行爲
origin:在任何情況下,僅發送文件的源作爲引用地址
origin-when-cross-origin: 對於同源的請求,會發送完整的URL作爲引用地址,但是對於非同源請求僅發送文件的源。
same-origin:對於同源的請求會發送引用地址,但是對於非同源請求則不發送引用地址信息。
上面只列舉了一部分可選策略,詳情可參考MDN文檔。

因此,我們可以手動指定no-referrer來隱藏referer

複製代碼 或者在創建image對象的時候,指定referrerPolicy策略

const img = new Image()
img.referrerPolicy = ‘no-referrer’
複製代碼
此時打開開發者工具就可以看見該圖片的請求已經不再攜帶對應的referer了。總結一下,一般有下面幾種設置Referer策略的方式:

通過 http 響應頭中的 Referrer-Policy 字段
通過 meta 標籤,name 爲 referrer
通過a、area、img、iframe、link元素的 referrerpolicy 屬性。
通過a、area、link元素的 rel=noreferrer 屬性。
需要注意的是目前referrerPolicy仍處於提案的草稿階段,瀏覽器兼容性並不是特別好。

在請求時修改header頭部
XMLHttpRequest對象提供了setRequestHeader方法,用於向請求頭添加或修改字段。我們能不能手動將修改 referer字段呢?

// 通過ajax下載圖片
function loadImage(uri) {
return new Promise(resolve => {
let xhr = new XMLHttpRequest();
xhr.responseType = “blob”;
xhr.onload = function() {
resolve(xhr.response);
};

    xhr.open("GET", uri, true);
    xhr.setRequestHeader("Referer", ""); // 通過setRequestHeader設置header不會生效
    xhr.send();
});

}

// 將下載下來的二進制大對象數據轉換成base64,然後展示在頁面上
function handleBlob(blob) {
let reader = new FileReader();
reader.onload = function(evt) {
img.src = evt.target.result;
};
reader.readAsDataURL(blob);
}

const imgSrc = “http://phptest2.com/upload/1.png”;
loadImage(imgSrc).then(blob => {
handleBlob(blob);
});
複製代碼
上述代碼運行時會發現控制檯提示錯誤

Refused to set unsafe header “Referer”

可以看見setRequestHeader設置referer響應頭是無效的,這是由於瀏覽器爲了安全起見,無法手動設置部分保留字段,不幸的是Referer恰好就是保留字段之一,詳情列表參考Forbidden header name。

因此在通過AJAX設置referer宣告失敗,那我們可以換一個方式從瀏覽器加載圖片,比如試試Fetch呢?

Fetch是瀏覽器提供的一個全新的接口,用於訪問和操作HTTP管道部分,該接口支持referrerPolicy,因此也可以用來操作referer。

function fetchImage(url) {
return fetch(url, {
headers: {
// “Referer”: “”, // 這裏設置無效
},
method: “GET”,
mode: “cors”,
redirect: “follow”,
referrer: “” // 將referer置空,此處寫成no-referrer貌似會把路徑替換成 host + 'no-referrer’字符串形式
}).then(response => response.blob());
}
loadImage(imgSrc).then(blob => {
handleBlob(blob);
});
複製代碼
通過將配置參數redirect置位空,可以看見本次請求已經不帶referer了。

使用iframe
下面是一種通過iframe來實現隱藏referer的方式,,整個過程有點魔性。大致實現如下

const putNoRefererImage = (() => {
let iframe
/*
src: 圖片地址
wrap:需要加載圖片的容器
*/
return function (src, wrap) {
if (iframe) {
iframe.remove()
}

    let url = new URL(src);
    let frameid = 'frameimg' + Math.random();
    window.img = `<img id="tmpImg" width=400 src="${url}" alt="圖片加載失敗,請稍後再試"/> `;

    // 構造一個iframe
    iframe = document.createElement('iframe')
    iframe.id = frameid
    iframe.src = "javascript:parent.img;" // 通過內聯的javascript,設置iframe的src
    // 校正iframe的尺寸,完整展示圖片
    iframe.onload = function () {
        var img = iframe.contentDocument.getElementById("tmpImg")
        if (img) {
            iframe.height = img.height + 'px'
            iframe.width = img.width + 'px'
        }
    }
    iframe.width = 200
    iframe.height = 200
    iframe.scrolling = "no"
    iframe.frameBorder = "0"
    wrap.appendChild(iframe)
}

})();

putNoRefererImage(imgSrc, document.body);
複製代碼
運行代碼可以看見,通過這種方式也可以實現隱藏referer的功能,因此用作不支持referrerPolicy的一種替代方案。

在某些不支持javascript內聯運行的場景下,這種方案也是不可行的,比如在chrome擴展程序,由於content_security_policy,使用內聯JavaScript會報錯

Refused to execute JavaScript URL because it violates the following Content Security Policy directive: “script-src ‘self’ blob: filesystem: chrome-extension-resource:”. Either the ‘unsafe-inline’ keyword, a hash (‘sha256-…’), or a nonce (‘nonce-…’) is required to enable inline execution.

本文總結了referer的作用,以及利用referer實現防盜鏈的配置。由於部分站點的防盜鏈配置允許referer爲空,因此可以利用這一點,通過隱藏referer,來達到繞開防盜鏈的目的。

接着介紹了幾種前端隱藏referer的實現方式,從技術上來看,referrerPolicy是近乎完美的選擇,由於存在兼容性限制,因此可以通過fetch或iframe等方式來實現。

2服務器nodejs 作防盜鏈圖片中轉

怎麼"破解防盜鏈"呢?
想要破解,就得先知道目標——防盜鏈如何實現。
大多數站點的策略很簡單: 判斷request請求頭的refer是否來源於本站。若不是,拒絕訪問真實圖片。

而我們知道: 請求頭是來自於客戶端,是可僞造的。

思路
那麼,我們僞造一個正確的refer來訪問不就行了?
整個業務邏輯大概像這樣:

自己的服務器後臺接受帶目標圖片url參數的請求
僞造refer請求目標圖片
把請求到的數據作爲response返回
這就起到了圖片中轉的作用。

  1. 項目是什麼樣子
    1.1 接口的樣子?
    有一個開放接口
    接口有一個參數,api?url=http://abc.com/image.png,大概長這樣子
    響應內容是反防盜鏈後的真實圖片
    1.2 應該怎麼做?
    把服務器跑起來
    處理 GET 請求
    分析請求參數
    下載原圖
    response 原圖
  2. 學習路徑(在對目標未知的前提下提出疑問)
    如何開始,建立服務器
    如何處理基本請求 GET POST
    如何下載圖片並轉發
    完成基本功能,上線
    優化
    2.1 如何開始,建立服務器
    主要是 http.createServer().listen(port) 這組方法,建立服務器、監聽端口一鍵搞定。

var http = require('http');
    
http.createServer(function (request, response) {
     // do things here
}).listen(8888);
    

console.log(‘Server running at: 8888’);
2.2 如何處理基本請求 GET POST
createServer 回調方法的兩個參數 req res 是 http request 和 response 的內容,打印一下他們的內容。

request 是 InComingMessage 類,打印它的 url 字段。

var http = require('http');
var url = require('url');
var util = require('util');
http.createServer(function(req, res){
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end(util.inspect(url.parse(req.url, true)));
}).listen(3000);
請求
http://localhost:3000/api?url=http://abc.com/image.png

請求結果

Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: '?url=http://abc.com/image.png',
  query: { url: 'http://abc.com/image.png' },
  pathname: '/api',
  path: '/api?url=http://abc.com/image.png',
  href: '/api?url=http://abc.com/image.png' }
query 字段剛好是我們想要的內容,下載這個字段對應的圖片。

2.3 如何下載圖片並轉發
request 模塊支持管道方法,可以和 shell 的管道一樣理解。

這可以省很多事,不需要在本地存儲圖片,不需要處理雜七雜八的事情,甚至不需要再去了解 nodejs 的流。一個方法全搞定。

關鍵方法: request(options).pipe(res)

var options = {
uri: imgUrl, // 這個 uri 爲空時,會認爲該字段不存在,報異常
headers: {
'Referer': referrer // 解決部分防盜鏈選項
}
};
request(options).pipe(res);

2.4 完成基本功能,上線
項目地址

完整代碼

    'use strict';
    var router = require('express').Router();
    var http = require('http');
    var url = require('url');
    var util = require('util');
    var fs = require('fs');
    var callfile = require('child_process');
    var request = require('request');
    
    router.get('/', function(req, res, next) {
        var imgUrl = url.parse(req.url, true).query.url;
        console.log(url.parse(req.url,true).query); 
    
        console.log('get a request for ' + imgUrl);
        if (imgUrl == null || imgUrl == "" || imgUrl == undefined) {
            console.log('end');
            res.end();
            return;
        }
    
        var parsedUrl = url.parse(imgUrl);
        // 這裏暫時使用圖片服務器主機名做Referer
        var referrer = parsedUrl.protocol + '//' + parsedUrl.host; 
        console.log('referrer ' + referrer);
    
        var options = {
          uri: imgUrl,
          headers: {
             'Referer': referrer
          }
        };
    
        function callback(error, response, body) {
          if (!error && response.statusCode == 200) {
            console.log("type " + response.headers['content-type']);
          }
          res.end(response.body);
        }
    
        // request(options, callback);
        request(options)
            .on('error', function(err) {
                console.log(err)
            })
            .pipe(res);
    });
    
    module.exports = router;

2.5 優化
這部分主要是防盜鏈部分的優化。

單就 Referer 來說,使用空值和主機名都只能滿足部分需求。

一個優化方式是組合,當一種方式不能突破即採用另一種方式。
這種方式的有點在於擴大了適用面積,並且方法對任何場景比較通用。

一個優化方式是接口請求參數帶源引用連接。
這種方式對很多人來說不太通用,因爲很多場景下並不清楚源引用連接在哪。
但是對我的插件來說非常適用,插件本身保留了源引用。因此可以很好的繞過防盜鏈限制。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章