對於內容型的公司,數據的安全性很重要。對於內容公司來講,數據的重要性不言而喻。好比你一個作在線教育的平臺,題目的數據很重要吧,可是被別人經過爬蟲技術所有爬走了?若是核心競爭力都被拿走了,那就是涼涼。再比說有個獨立開發者想抄襲你的產品,經過抓包和爬蟲手段將你核心的數據拿走,而後短時間內作個網站和 App,短時間內成爲你的勁敵。javascript
本人從這2個角度(網頁所見非所得、查接口請求沒用)出發,制定了下面的反爬方案。css
使用HTTPS 協議html
單位時間內限制掉請求次數過多,則封鎖該帳號前端
前端技術限制 (接下來是核心技術)java
# 好比須要正確顯示的數據爲「19950220」 1. 先按照本身需求利用相應的規則(數字亂序映射,好比正常的0對應仍是0,可是亂序就是 0 <-> 1,1 <-> 9,3 <-> 8,...)製做自定義字體(ttf) 2. 根據上面的亂序映射規律,求獲得須要返回的數據 19950220 -> 17730220 3. 對於第一步獲得的字符串,依次遍歷每一個字符,將每一個字符根據按照線性變換(y=kx+b)。線性方程的係數和常數項是根據當前的日期計算獲得的。好比當前的日期爲「2018-07-24」,那麼線性變換的 k 爲 7,b 爲 24。 4. 而後將變換後的每一個字符串用「3.1415926」拼接返回給接口調用者。(爲何是3.1415926,由於對數字僞造反爬,因此拼接的文本確定是數字的話不太會引發研究者的注意,可是數字長度過短會誤傷正常的數據,因此用所熟悉的 Π) ``` 1773 -> 「1*7+24」 + 「3.1415926」 + 「7*7+24」 + 「3.1415926」 + 「7*7+24」 + 「3.1415926」 + 「3*7+24」 -> 313.1415926733.1415926733.141592645 02 -> "0*7+24" + "3.1415926" + "2*7+24" -> 243.141592638 20 -> "2*7+24" + "3.1415926" + "0*7+24" -> 383.141592624 ``` # 前端拿到數據後再解密,解密後根據自定義的字體 Render 頁面 1. 先將拿到的字符串按照「3.1415926」拆分爲數組 2. 對數組的每1個數據,按照「線性變換」(y=kx+b,k和b一樣按照當前的日期求解獲得),逆向求解到本來的值。 3. 將步驟2的的到的數據依次拼接,再根據 ttf 文件 Render 頁面上。
下面以 Node.js 爲例講解後端須要作的事情node
首前後端設置接口路由webpack
獲取路由後面的參數git
根據業務須要根據 SQL 語句生成對應的數據。若是是數字部分,則須要按照上面約定的方法加以轉換。github
將生成數據轉換成 JSON 返回給調用者web
// json var JoinOparatorSymbol = "3.1415926"; function encode(rawData, ruleType) { if (!isNotEmptyStr(rawData)) { return ""; } var date = new Date(); var year = date.getFullYear(); var month = date.getMonth() + 1; var day = date.getDate(); var encodeData = ""; for (var index = 0; index < rawData.length; index++) { var datacomponent = rawData[index]; if (!isNaN(datacomponent)) { if (ruleType < 3) { var currentNumber = rawDataMap(String(datacomponent), ruleType); encodeData += (currentNumber * month + day) + JoinOparatorSymbol; } else if (ruleType == 4) { encodeData += rawDataMap(String(datacomponent), ruleType); } else { encodeData += rawDataMap(String(datacomponent), ruleType) + JoinOparatorSymbol; } } else if (ruleType == 4) { encodeData += rawDataMap(String(datacomponent), ruleType); } } if (encodeData.length >= JoinOparatorSymbol.length) { var lastTwoString = encodeData.substring(encodeData.length - JoinOparatorSymbol.length, encodeData.length); if (lastTwoString == JoinOparatorSymbol) { encodeData = encodeData.substring(0, encodeData.length - JoinOparatorSymbol.length); } }
//字體映射處理 function rawDataMap(rawData, ruleType) { if (!isNotEmptyStr(rawData) || !isNotEmptyStr(ruleType)) { return; } var mapData; var rawNumber = parseInt(rawData); var ruleTypeNumber = parseInt(ruleType); if (!isNaN(rawData)) { lastNumberCategory = ruleTypeNumber; //字體文件1下的數據加密規則 if (ruleTypeNumber == 1) { if (rawNumber == 1) { mapData = 1; } else if (rawNumber == 2) { mapData = 2; } else if (rawNumber == 3) { mapData = 4; } else if (rawNumber == 4) { mapData = 5; } else if (rawNumber == 5) { mapData = 3; } else if (rawNumber == 6) { mapData = 8; } else if (rawNumber == 7) { mapData = 6; } else if (rawNumber == 8) { mapData = 9; } else if (rawNumber == 9) { mapData = 7; } else if (rawNumber == 0) { mapData = 0; } } //字體文件2下的數據加密規則 else if (ruleTypeNumber == 0) { if (rawNumber == 1) { mapData = 4; } else if (rawNumber == 2) { mapData = 2; } else if (rawNumber == 3) { mapData = 3; } else if (rawNumber == 4) { mapData = 1; } else if (rawNumber == 5) { mapData = 8; } else if (rawNumber == 6) { mapData = 5; } else if (rawNumber == 7) { mapData = 6; } else if (rawNumber == 8) { mapData = 7; } else if (rawNumber == 9) { mapData = 9; } else if (rawNumber == 0) { mapData = 0; } } //字體文件3下的數據加密規則 else if (ruleTypeNumber == 2) { if (rawNumber == 1) { mapData = 6; } else if (rawNumber == 2) { mapData = 2; } else if (rawNumber == 3) { mapData = 1; } else if (rawNumber == 4) { mapData = 3; } else if (rawNumber == 5) { mapData = 4; } else if (rawNumber == 6) { mapData = 8; } else if (rawNumber == 7) { mapData = 3; } else if (rawNumber == 8) { mapData = 7; } else if (rawNumber == 9) { mapData = 9; } else if (rawNumber == 0) { mapData = 0; } } else if (ruleTypeNumber == 3) { if (rawNumber == 1) { mapData = ""; } else if (rawNumber == 2) { mapData = ""; } else if (rawNumber == 3) { mapData = ""; } else if (rawNumber == 4) { mapData = ""; } else if (rawNumber == 5) { mapData = ""; } else if (rawNumber == 6) { mapData = ""; } else if (rawNumber == 7) { mapData = ""; } else if (rawNumber == 8) { mapData = ""; } else if (rawNumber == 9) { mapData = ""; } else if (rawNumber == 0) { mapData = ""; } } else{ mapData = rawNumber; } } else if (ruleTypeNumber == 4) { var sources = ["年", "萬", "業", "人", "信", "元", "千", "司", "州", "資", "造", "錢"]; //判斷字符串爲漢字 if (/^[\u4e00-\u9fa5]*$/.test(rawData)) { if (sources.indexOf(rawData) > -1) { var currentChineseHexcod = rawData.charCodeAt(0).toString(16); var lastCompoent; var mapComponetnt; var numbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; var characters = ["a", "b", "c", "d", "e", "f", "g", "h", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]; if (currentChineseHexcod.length == 4) { lastCompoent = currentChineseHexcod.substr(3, 1); var locationInComponents = 0; if (/[0-9]/.test(lastCompoent)) { locationInComponents = numbers.indexOf(lastCompoent); mapComponetnt = numbers[(locationInComponents + 1) % 10]; } else if (/[a-z]/.test(lastCompoent)) { locationInComponents = characters.indexOf(lastCompoent); mapComponetnt = characters[(locationInComponents + 1) % 26]; } mapData = "&#x" + currentChineseHexcod.substr(0, 3) + mapComponetnt + ";"; } } else { mapData = rawData; } } else if (/[0-9]/.test(rawData)) { mapData = rawDataMap(rawData, 2); } else { mapData = rawData; } } return mapData; }
//api module.exports = { "GET /api/products": async (ctx, next) => { ctx.response.type = "application/json"; ctx.response.body = { products: products }; }, "GET /api/solution1": async (ctx, next) => { try { var data = fs.readFileSync(pathname, "utf-8"); ruleJson = JSON.parse(data); rule = ruleJson.data.rule; } catch (error) { console.log("fail: " + error); } var data = { code: 200, message: "success", data: { name: "@杭城小劉", year: LBPEncode("1995", rule), month: LBPEncode("02", rule), day: LBPEncode("20", rule), analysis : rule } } ctx.set("Access-Control-Allow-Origin", "*"); ctx.response.type = "application/json"; ctx.response.body = data; }, "GET /api/solution2": async (ctx, next) => { try { var data = fs.readFileSync(pathname, "utf-8"); ruleJson = JSON.parse(data); rule = ruleJson.data.rule; } catch (error) { console.log("fail: " + error); } var data = { code: 200, message: "success", data: { name: LBPEncode("建造師",rule), birthday: LBPEncode("1995年02月20日",rule), company: LBPEncode("中天公司",rule), address: LBPEncode("浙江省杭州市拱墅區石祥路",rule), bidprice: LBPEncode("2萬元",rule), negative: LBPEncode("2018年辦事效率過高、負面基本沒有",rule), title: LBPEncode("建造師",rule), honor: LBPEncode("最佳獎",rule), analysis : rule } } ctx.set("Access-Control-Allow-Origin", "*"); ctx.response.type = "application/json"; ctx.response.body = data; }, "POST /api/products": async (ctx, next) => { var p = { name: ctx.request.body.name, price: ctx.request.body.price }; products.push(p); ctx.response.type = "application/json"; ctx.response.body = p; } };
//路由 const fs = require("fs"); function addMapping(router, mapping){ for(var url in mapping){ if (url.startsWith("GET")) { var path = url.substring(4); router.get(path,mapping[url]); console.log(`Register URL mapping: GET: ${path}`); }else if (url.startsWith('POST ')) { var path = url.substring(5); router.post(path, mapping[url]); console.log(`Register URL mapping: POST ${path}`); } else if (url.startsWith('PUT ')) { var path = url.substring(4); router.put(path, mapping[url]); console.log(`Register URL mapping: PUT ${path}`); } else if (url.startsWith('DELETE ')) { var path = url.substring(7); router.del(path, mapping[url]); console.log(`Register URL mapping: DELETE ${path}`); } else { console.log(`Invalid URL: ${url}`); } } } function addControllers(router, dir){ fs.readdirSync(__dirname + "/" + dir).filter( (f) => { return f.endsWith(".js"); }).forEach( (f) => { console.log(`Process controllers:${f}...`); let mapping = require(__dirname + "/" + dir + "/" + f); addMapping(router,mapping); }); } module.exports = function(dir){ let controllers = dir || "controller"; let router = require("koa-router")(); addControllers(router,controllers); return router.routes(); };
前端根據服務端返回的數據逆向解密
$("#year").html(getRawData(data.year,log)); // util.js var JoinOparatorSymbol = "3.1415926"; function isNotEmptyStr($str) { if (String($str) == "" || $str == undefined || $str == null || $str == "null") { return false; } return true; } function getRawData($json,analisys) { $json = $json.toString(); if (!isNotEmptyStr($json)) { return; } var date= new Date(); var year = date.getFullYear(); var month = date.getMonth() + 1; var day = date.getDate(); var datacomponents = $json.split(JoinOparatorSymbol); var orginalMessage = ""; for(var index = 0;index < datacomponents.length;index++){ var datacomponent = datacomponents[index]; if (!isNaN(datacomponent) && analisys < 3){ var currentNumber = parseInt(datacomponent); orginalMessage += (currentNumber - day)/month; } else if(analisys == 3){ orginalMessage += datacomponent; } else{ //其餘狀況待續,本 Demo 根據本人在研究反爬方面的技術並實踐後持續更新 } } return orginalMessage; }
好比後端返回的是323.14743.14743.1446,根據咱們約定的算法,能夠的到結果爲1773
根據 ttf 文件 Render 頁面 上面計算的到的1773,而後根據ttf文件,頁面看到的就是1995
而後爲了防止爬蟲人員查看 JS 研究問題,因此對 JS 的文件進行了加密處理。若是你的技術棧是 Vue 、React 等,webpack 爲你提供了 JS 加密的插件,也很方便處理
我的以爲這種方式還不是很安全。因而想到了各類方案的組合拳。好比
我的以爲若是一個前端經驗豐富的爬蟲開發者來講,上面的方案可能仍是會存在被破解的可能,因此在以前的基礎上作了升級版本
這幾種組合拳打下來。對於通常的爬蟲就放棄了。
上面說的方法主要是針對數字作的反爬手段,若是要對漢字進行反爬怎麼辦?接下來提供幾種方案
方案1: 對於你站點頻率最高的詞雲,作一個漢字映射,也就是自定義字體文件,步驟跟數字同樣。先將經常使用的漢字生成對應的 ttf 文件;根據下面提供的連接,將 ttf 文件轉換爲 svg 文件,而後在下面的「字體映射」連接點進去的網站上面選擇前面生成的 svg 文件,將svg文件裏面的每一個漢字作個映射,也就是將漢字專爲 unicode 碼(注意這裏的 unicode 碼不要去在線直接生成,由於直接生成的東西也就是有規律的。我給的作法是先用網站生成,而後將獲得的結果作個簡單的變化,好比將「e342」轉換爲 「e231」);而後接口返回的數據按照咱們的這個字體文件的規則反過去映射出來。
方案2: 將網站的重要字體,將 html 部分生成圖片,這樣子爬蟲要識別到須要的內容成本就很高了,須要用到 OCR。效率也很低。因此能夠攔截掉一部分的爬蟲
方案3: 看到攜程的技術分享「反爬的最高境界就是 Canvas 的指紋,原理是不一樣的機器不一樣的硬件對於 Canvas 畫出的圖老是存在像素級別的偏差,所以咱們判斷當對於訪問來講大量的 canvas 的指紋一致的話,則認爲是爬蟲,則能夠封掉它」。
本人將方案1實現到 Demo 中了。
//style.css @font-face { font-family: "NumberFont"; src: url('http://127.0.0.1:8080/Util/analysis'); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @font-face { font-family: "CharacterFont"; src: url('http://127.0.0.1:8080/Util/map'); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } h2 { font-family: "NumberFont"; } h3,a{ font-family: "CharacterFont"; }
這幾種組合拳打下來。對於通常的爬蟲就放棄了。
<hr>
前面的 ttf 轉 svg 網站當 ttf 文件太大會限制轉換,讓你購買,下面貼出個新的連接。
運行步驟
//客戶端。先查看本機 ip 在 Demo/Spider-develop/Solution/Solution1.js 和 Demo/Spider-develop/Solution/Solution2.js 裏面將接口地址修改成本機 ip $ cd Demo $ ls REST Spider-release file-Server.js Spider-develop Util rule.json $ node file-Server.js Server is runnig at http://127.0.0.1:8080/ //服務端 先安裝依賴 $ cd REST/ $ npm install $ node app.js