在反爬蟲首部曲 - 如何防治初級網頁爬蟲?的文章當中,已經跟各位朋友大致上介紹過,這次防治爬蟲案例中的情境及所使用到的工具,防治初級爬蟲算是容易的,接下來我們要來防治中級爬蟲,難度上會稍微高一點,而且一樣要撰寫一些程式碼。
何謂中級爬蟲?
中級爬蟲在我自己的定義上,一樣是會使用 HTTP 客戶端來爬資料,只是這類的爬蟲對於 HTTP Request/Response 的互動過程有一定程度的了解,他們可以向 Web Api 發送合法的 Request,造成後端在爬蟲的判定上有一定的困難度,無法分辨 Request 是真實使用者發送過來的?還是爬蟲發送過來的?而我們遇到的大多數爬蟲都是屬於中級爬蟲,因此在防治上我們必須多下點工夫,防治手段我分成三個步驟來進行。
第一步驟:驗
第一個步驟「驗
」,就是驗證
,在這系列的文章中所描述的情境,Web Api 皆是提供給執行在瀏覽器的前端程式來呼叫的,因此「瀏覽器
」就成了我們可以用來防治爬蟲的工具,我們可以在網頁載入的一開始預埋一段演算法,這段演算法產生一個「簽章(Signature)
」,在呼叫 Web Api 時夾帶在 Request Headers 裡面,後端程式執行相同的演算法進行簽章的驗證,驗證通過才算是合法的呼叫。
產生簽章的演算法非常的彈性,我們可以視需求去挑選我們要的資訊來做,我這邊展示幾個我用到的資訊:
Fingerprint
:透過 FingerprintJS 這個套件取得的一個客戶端的指紋資訊,它使用一些機制盡可能地為每個客戶端產生唯一的識別碼,相關原理請參考它的官網,這邊不多做說明。UUID
:就是一個 UUID,原來是追蹤瀏覽軌跡用的,這是瀏覽網頁必定會產生的資料。User-Agent
:透過window.navigator.userAgent
取得參考網頁路徑(Referer)
:透過window.location
取得密鑰
:一串自定義的密碼
關於產生的方式,我這邊是把這幾個資訊串起來,透過 SHA384 做雜湊,然後把演算法與 AJAX 的設定結合起來,在發送 Request 之前執行一次產生簽章的演算法,將它加進 Request Headers 裡面,其中 Fingerprint 及 UUID 會被帶進 Cookie ,大致上就像下面的程式碼:
signature.js
var http = (function () {
var _defaultSettings;
return {
get defaultSettings() {
return typeof _defaultSettings === "function" ? _defaultSettings() : _defaultSettings;
},
set defaultSettings(val) {
_defaultSettings = val;
}
};
})();
(function () {
// 產生 UUID
const u = Cookies.get("UUID") || (function () {
let d = Date.now();
if (typeof performance !== "undefined" && typeof performance.now === "function") {
d += performance.now();
}
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
let r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16);
});
})().toUpperCase();
Cookies.remove("UUID");
Cookies.set("UUID", u, { expires: 365 });
// 簽章演算法與 AJAX 設定結合
http.defaultSettings = (function () {
const dfd = $.Deferred();
// 產生客戶端指紋
FingerprintJS.load()
.then(fp => fp.get())
.then(r => {
Cookies.set("fingerprint", r.visitorId, { expires: 365 });
dfd.resolve();
});
return function () {
return dfd.promise().then(() => {
// 產生簽章
const f = Cookies.get("fingerprint");
const s = f + u + window.navigator.userAgent + window.location.toString() + "kTwpZe9DS7NnARXn";
const h = CryptoJS.SHA384(s).toString();
return { headers: { "X-Signature": h } }
});
};
})();
})();
Index.cshtml
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<!-- ... -->
<div id="my-api"></div>
</div>
@section Scripts
{
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/3.0.1/js.cookie.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/x64-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/sha512.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/sha384.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@@fingerprintjs/fingerprintjs@3.3.3/dist/fp.min.js"></script>
<script src="/js/signature.js" asp-append-version="true"></script>
<script>
$(function () {
http.defaultSettings.then(function (defaultSettings) {
var settings = $.extend(true, { method: "GET" }, defaultSettings);
$.ajax("/Home/MyApi", settings)
.then(function (result) {
$("#my-api").text(result.value);
});
});
});
</script>
}
前端搞定之後我們再來處理後端,我直接建一個 Action Filter 給需要防治爬蟲的 Web Api 使用,在 Action Filter 裡面我們除了進行簽章的檢查之外,在這之前還可以先檢查組成簽章的元素,發送來的 Request 如果沒有這些組成簽章的元素,就可以先擋掉了。
public class AntiCrawlerAttribute : Attribute, IAsyncActionFilter, IOrderedFilter
{
public int Order { get; set; } = -99;
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (!HasUUID(out var uuid))
{
context.Result = BadRequestContent();
}
else if (!HasFingerprint(out var fingerprint))
{
context.Result = BadRequestContent();
}
else if (!HasSignature(out var clientSignature))
{
context.Result = BadRequestContent();
}
else if (!HasReferer(out var referer))
{
context.Result = BadRequestContent();
}
else if (!HasUserAgent(out var userAgent))
{
context.Result = BadRequestContent();
}
else if (ValidSignature(clientSignature, userAgent, fingerprint, uuid, referer))
{
await next();
}
else
{
context.Result = BadRequestContent();
}
ContentResult BadRequestContent()
{
return new ContentResult { Content = $"400 - {(HttpStatusCode)400}", StatusCode = 400 };
}
bool HasSignature(out StringValues signature)
{
if (!context.HttpContext.Request.Headers.TryGetValue("X-Signature", out signature)) return false;
if (string.IsNullOrEmpty(signature)) return false;
return true;
}
bool HasFingerprint(out string fingerprint)
{
if (!context.HttpContext.Request.Cookies.TryGetValue("fingerprint", out fingerprint)) return false;
if (string.IsNullOrEmpty(fingerprint)) return false;
return true;
}
bool HasUUID(out string uuid)
{
if (!context.HttpContext.Request.Cookies.TryGetValue("UUID", out uuid)) return false;
if (string.IsNullOrEmpty(uuid)) return false;
return true;
}
bool HasUserAgent(out StringValues userAgent)
{
if (!context.HttpContext.Request.Headers.TryGetValue(HeaderNames.UserAgent, out userAgent)) return false;
if (string.IsNullOrEmpty(userAgent)) return false;
return true;
}
bool HasReferer(out StringValues referer)
{
if (!context.HttpContext.Request.Headers.TryGetValue(HeaderNames.Referer, out referer)) return false;
if (string.IsNullOrEmpty(referer)) return false;
return true;
}
bool ValidSignature(string clientSignature, string userAgent, string fingerprint, string uuid, string referer)
{
var signature = $"{fingerprint}{uuid}{userAgent}{referer}kTwpZe9DS7NnARXn";
var signatureHash = BitConverter.ToString(SHA384.Create().ComputeHash(Encoding.UTF8.GetBytes(signature)))
.Replace("-", string.Empty)
.ToLower();
return clientSignature == signatureHash;
}
}
}
第一步驟完成之後,正常會在畫面中看到 Hello World
的字樣。
如果我們直接去打 /Home/MyApi,應該會看到 400 - BadRequest
的訊息。
第二步驟:變
第二個步驟「變
」,意思是簽章要時刻在改變,我們加入時間元素,讓簽章具有時效性,我們可以這樣做,將 UNIX Timestamp 除以一個秒數取商數,當成組成簽章的第 6 個資訊,我這邊是除以 60 秒。
後端的部分,由於前後端可能有時間差,所以後端允許 ±1 的緩衝。
這樣一來,簽章的值就不固定了,會隨著時間而改變,即使 Request 被整個捕捉下來,簽章也很快就失效了。
第三步驟:藏
第三個步驟是「藏
」,即隱藏
,我們的演算法在前端是用 JavaScript 寫的,形同裸奔,在這邊我們要使用著名的程式碼混餚工具 - JavaScript Obfuscator Tool,將我們的簽章演算法原始碼進行高度混餚,使用的方法很簡單,複製貼上之後,Options Preset 選項選擇 High
,接著將 Disable Console Output
、Self Defending
、Debug Protection
取消勾選,最後點擊 Obfuscate
就行了。
兵不厭詐,把混餚過後的程式碼貼到 js 檔之中,這個 js 檔另外命名為其他知名套件的名稱,形成資訊上的落差,我們發佈出去的網頁就改引用這個偽裝的 js 檔,甚至把混餚過後的程式碼,附加在知名套件的程式碼檔案的後面也可以,儘量藏在不容易被知道的地方。
經過了三個步驟:驗、變、藏,基本上不清楚規則的爬蟲,就比較難從我們的 Web Api 持續地抓到資料,其實有爬蟲來抓我們的資料,代表我們的資料有那麼一點價值,可以思考是不是有收費的可能,以上是中級爬蟲的防治方法,提供給大家參考,希望對大家有一點點幫助。