摘要:對于內容型的公司,數據的安全性很重要。背景目前通過中的網頁分析后,我們的數據安全性做的較差,有以下幾個點存在問題網站的數據通過最早期的前后端分離來實現。比如當前的日期為,那么線性變換的為,為。
之前在上家公司的時候做過一些爬蟲的工作,也幫助爬蟲工程師解決過一些問題。然后我寫過一些文章發布到網上,之后有一些人就找我做一些爬蟲的外包,內容大概是爬取小紅書的用戶數據和商品數據,但是我沒做。我覺得對于國內的大數據公司沒幾家是有真正的大數據量,而是通過爬蟲工程師團隊不斷的去各地爬取數據,因此不要以為我們的數據沒價值,對于內容型的公司來說,數據是可信競爭力。那么我接下來想說的就是網絡和數據的安全性問題。背景
對于內容型的公司,數據的安全性很重要。對于內容公司來說,數據的重要性不言而喻。比如你一個做在線教育的平臺,題目的數據很重要吧,但是被別人通過爬蟲技術全部爬走了?如果核心競爭力都被拿走了,那就是涼涼。再比說有個獨立開發者想抄襲你的產品,通過抓包和爬蟲手段將你核心的數據拿走,然后短期內做個網站和 App,短期內成為你的勁敵。
目前通過 App 中的 網頁分析后,我們的數據安全性做的較差,有以下幾個點存在問題:
網站的數據通過最早期的前后端分離來實現。稍微學過 Web 前端的工程師都可以通過神器 Chrome 分析網站,進而爬取需要的數據。打開 「Network」就可以看到網站的所有網絡請求了,哎呀,不小心我看到了什么?沒錯就是網站的接口信息都可以看到了。比如 “detail.json?itemId=141529859”。或者你的網站接口有些特殊的判斷處理,將一些信息存儲到 sessionStorage、cookie、localStorage 里面,有點前端經驗的爬蟲工程師心想”嘿嘿嘿,這不是在裸奔數據么“。或者有些參數是通過 JavaScript 臨時通過函數生成的。問題不大,工程師也可以對網頁元素進行查找,找到關鍵的 id、或者 css 類名,然后在 "Search“ 可以進行查找,找到對應的代碼 JS 代碼,點擊查看代碼,如果是早期前端開發模式那么代碼就是裸奔的,跟開發者在自己的 IDE 里面看到的內容一樣,有經驗的爬蟲就可以拿這個做事情,因此安全性問題亟待解決。
想知道 Chrome 更多的調試使用技巧,看看這篇文章
App 的數據即使采用了 HTTPS,但是對于專業的抓包工具也是可以直接拿到數據的,因此 App 的安全問題也可以做一些提高,具體的策略下文會講到。
想知道 Charles 的更多使用技巧,可以看看這篇文章
爬蟲手段目前爬蟲技術都是從渲染好的 html 頁面直接找到感興趣的節點,然后獲取對應的文本
有些網站安全性做的好,比如列表頁可能好獲取,但是詳情頁就需要從列表頁點擊對應的 item,將 itemId 通過 form 表單提交,服務端生成對應的參數,然后重定向到詳情頁(重定向過來的地址后才帶有詳情頁的參數 detailID),這個步驟就可以攔截掉一部分的爬蟲開發者
解決方案 制定出Web 端反爬技術方案本人從這2個角度(網頁所見非所得、查接口請求沒用)出發,制定了下面的反爬方案。
使用HTTPS 協議
單位時間內限制掉請求次數過多,則封鎖該賬號
前端技術限制 (接下來是核心技術)
# 比如需要正確顯示的數據為“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 為例講解后端需要做的事情
首先后端設置接口路由
獲取路由后面的參數
根據業務需要根據 SQL 語句生成對應的數據。如果是數字部分,則需要按照上面約定的方法加以轉換。
將生成數據轉換成 JSON 返回給調用者
// 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 = "" + 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 加密的插件,也很方便處理
JS混淆工具
個人覺得這種方式還不是很安全。于是想到了各種方案的組合拳。比如
反爬升級版個人覺得如果一個前端經驗豐富的爬蟲開發者來說,上面的方案可能還是會存在被破解的可能,所以在之前的基礎上做了升級版本
組合拳1: 字體文件不要固定,雖然請求的鏈接是同一個,但是根據當前的時間戳的最后一個數字取模,比如 Demo 中對4取模,有4種值 0、1、2、3。這4種值對應不同的字體文件,所以當爬蟲絞盡腦汁爬到1種情況下的字體時,沒想到再次請求,字體文件的規則變掉了
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/101042.html
摘要:更詳細的內容下一章開篇深入聊聊前后分離講述關于我目前在寫從零構建前后分離項目系列,修正和補充以此為準不斷更新的項目實踐地址彩蛋提前預覽下一章傳送門 開篇 : 縱觀WEB歷史演變 在校學習和幾年工作工作中不知不覺經歷了一半的 WEB 歷史演變、對近幾年的發展比較了解,結合經驗聊聊 WEB 發展歷史。 演變不易,但也是必然,因為為人始終要進步。 WEB 的發展史 一、開山鼻祖 - 石器時代...
摘要:更詳細的內容下一章開篇深入聊聊前后分離講述關于我目前在寫從零構建前后分離項目系列,修正和補充以此為準不斷更新的項目實踐地址彩蛋提前預覽下一章傳送門 開篇 : 縱觀WEB歷史演變 在校學習和幾年工作工作中不知不覺經歷了一半的 WEB 歷史演變、對近幾年的發展比較了解,結合經驗聊聊 WEB 發展歷史。 演變不易,但也是必然,因為為人始終要進步。 WEB 的發展史 一、開山鼻祖 - 石器時代...
摘要:大局意識就是合理地優先處理某些重要的小項目。對于所做事情的意義,我想到了美國西部大淘金的時代。我在美國時發現很多產業都只剩下幾個巨頭在競爭,而國內各種類似領域有大量小公司的存在。 Integ 是 SegmentFault 的前端 Hacker,在本次訪談中貢獻了編程五年多的感悟與總結。Codes Dont Lie 這個標題(并非 Hips Dont Lie)代表了 Integ 的誠懇和...
摘要:前端性能優化的涉及點從服務器到協議再到宿主環境本身都要有比較深刻的認識,業界目前主要還是以雅虎總結出來條前端性能優化的黃金軍規為參考。 歡迎大家前往騰訊云技術社區,獲取更多騰訊海量技術實踐干貨哦~ 導語 : 從事前端有6年+的時間了,從最開始的美工到重構再到偏向js邏輯開發的前端開發,一直在前端這個行業里面摸索和學習,我現在將自己這些年的一個心得體會來個系統性的梳理寫成一篇關于性能優化...
閱讀 1958·2021-11-16 11:45
閱讀 3668·2021-09-06 15:02
閱讀 2013·2019-08-30 15:44
閱讀 2283·2019-08-30 11:21
閱讀 1845·2019-08-29 16:31
閱讀 3422·2019-08-29 13:55
閱讀 1895·2019-08-29 12:15
閱讀 3251·2019-08-28 18:05