之前在上家公司的时刻做过一些爬虫的事变,也协助爬虫工程师处置惩罚过一些题目。然后我写过一些文章宣布到网上,以后有一些人就找我做一些爬虫的外包,内容大概是爬取小红书的用户数据和商品数据,然则我没做。我认为关于国内的大数据公司没几家是有真正的大数据量,而是经由过程爬虫工程师团队不停的去各地爬取数据,因而不要认为我们的数据没代价,关于内容型的公司来讲,数据是可托竞争力。那末我接下来想说的就是收集和数据的平安性题目。
关于内容型的公司,数据的平安性很主要。关于内容公司来讲,数据的主要性显而易见。比方你一个做在线教育的平台,题目标数据很主要吧,然则被他人经由过程爬虫手艺悉数爬走了?假如中心竞争力都被拿走了,那就是凉凉。再比说有个自力开辟者想剽窃你的产物,经由过程抓包和爬虫手腕将你中心的数据拿走,然后短期内做个网站和 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 = "&#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: 字体文件不要牢固,虽然要求的链接是同一个,然则依据当前的时候戳的末了一个数字取模,比方 Demo 中对4取模,有4种值 0、1、2、3。这4种值对应差别的字体文件,所以当爬虫挖空心思爬到1种状况下的字体时,没想到再次要求,字体文件的划定规矩变掉了 😂
- 组合拳2: 前面的划定规矩是字体题目乱序,然则只是数字婚配打乱掉。比方 1 -> 4, 5 -> 8。接下来的套路就是每一个数字对应一个 unicode 码 ,然后制造本身须要的字体,能够是 .ttf、.woff 等等。
这几种组合拳打下来。关于平常的爬虫就摒弃了。
反爬手腕再晋级
上面说的要领主如果针对数字做的反爬手腕,假如要对汉字举行反爬怎么办?接下来供应几种计划
- 计划1: 关于你站点频次最高的词云,做一个汉字映照,也就是自定义字体文件,步骤跟数字一样。先将经常使用的汉字天生对应的 ttf 文件;依据下面供应的链接,将 ttf 文件转换为 svg 文件,然后鄙人面的“字体映照”链接点进去的网站上面挑选前面天生的 svg 文件,将svg文件内里的每一个汉字做个映照,也就是将汉字专为 unicode 码(注重这里的 unicode 码不要去在线直接天生,由于直接天生的东西也就是有规律的。我给的做法是先用网站天生,然后将获得的结果做个简朴的变化,比方将“e342”转换为 “e231”);然后接口返回的数据根据我们的这个字体文件的划定规矩反过去映照出来。
- 计划2: 将网站的主要字体,将 html 部份天生图片,这模样爬虫要识别到须要的内容本钱就很高了,须要用到 OCR。效力也很低。所以能够拦截掉一部份的爬虫
- 计划3: 看到携程的手艺分享“反爬的最高境地就是 Canvas 的指纹,道理是差别的机械差别的硬件关于 Canvas 画出的图老是存在像素级别的偏差,因而我们推断当关于接见来讲大批的 canvas 的指纹一致的话,则认为是爬虫,则能够封掉它”。
本人将计划1完成到 Demo 中了。
症结步骤
- 先依据你们的产物找到经常使用的症结词,天生词云
- 依据词云,将每一个字天生对应的 unicode 码
- 将词云包含的汉字做成一个字体库
- 将字体库 .ttf 做成 svg 花样,然后上传到 icomoon 制造自定义的字体,然则有划定规矩,比方 “年” 对应的 unicode 码是 “u5e74” ,然则我们须要做一个 恺撒加密 ,比方我们设置 偏移量 为1,那末经由恺撒加密 “年”对应的 unicode 码是“u5e75” 。应用这类划定规矩制造我们须要的字体库
- 在每次挪用接口的时刻效劳端做的事变是:效劳端封装某个要领,将数据经由要领推断是否是在词云中,假如是词云中的字符,应用划定规矩(找到汉字对应的 unicode 码,再依据凯撒加密,设置对应的偏移量,Demo 中为1,将每一个汉字加密处置惩罚)加密处置惩罚后返回数据
客户端做的事变:
- 先引入我们前面制造好的汉字字体库
- 挪用接口拿到数据,显现到对应的 Dom 节点上
- 假如是汉字文本,我们将对应节点的 css 类设置成汉字类,该类对应的 font-family 是我们上面引入的汉字字体库
//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";
}
传送门
完成的结果
- 页面上看到的数据跟检察元素看到的结果不一致
- 去检察接口数据跟考核元素和界面看到的三者不一致
- 页面每次革新之前得出的结果更不一致
- 关于数字和汉字的处置惩罚手腕都不一致
这几种组合拳打下来。关于平常的爬虫就摒弃了。
前面的 ttf 转 svg 网站当 ttf 文件太大会限定转换,让你购置,下面贴出个新的链接。
Demo 地点
运转步骤
//客户端。先检察本机 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
App 端平安的处置惩罚计划
现在 App 的收集通信基础都是用 HTTPS 的效劳,然则随意一个抓包东西都是能够看到 HTTPS 接口的细致数据,为了做到防备抓包和没法模拟接口的状况,我们采纳以下步伐:
- 中心人盗用数据,我们能够采纳 HTTPS 证书的双向认证,这模样完成的结果就是中心人在开启抓包软件剖析 App 的收集要求的时刻,收集会自动断掉,没法检察剖析要求的状况
- 关于防备用户模拟我们的要求再次提议要求,我们能够采纳 「防重放战略」,用户再也没法模拟我们的要求,再次去猎取数据了。
- 关于 App 内的 H5 资本,反爬虫计划能够采纳上面的处置惩罚计划,H5 内部的收集要求能够经由过程 Hybrid 层让 Native 的才能去完成收集要求,完成以后将数据回调给 JS。这么做的目标是每每我们的 Native 层有完美的账号系统和收集层以及优越的平安战略、鉴权系统等等。
- 后期会议论 App 平安性的更深条理弄法,比方从逆向的角度动身怎样庇护 App 的平安性。提早给出一篇逆向平安方面的文章
关于 Hybrid 的更多内容,能够看看这篇文章 Awesome Hybrid
比方 JS 须要提议一个收集要求,那末根据上面将收集要求让 Native 去完成,然后回调给 JS
JS 端代码
var requestObject = { url: arg.Api + "SearchInfo/getLawsInfo", params: requestparams, Hybrid_Request_Method: 0 }; requestHybrid({ tagname: 'NativeRequest', param: requestObject, encryption: 1, callback: function (data) { renderUI(data); } })
Native 代码(iOS为例)
[self.bridge registerHandler:@"NativeRequest" handler:^(id data, WVJBResponseCallback responseCallback) { NSAssert([data isKindOfClass:[NSDictionary class]], @"H5 端不按套路"); if ([data isKindOfClass:[NSDictionary class]]) { NSDictionary *dict = (NSDictionary *)data; RequestModel *requestModel = [RequestModel yy_modelWithJSON:dict]; NSAssert( (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Post) || (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Get ), @"H5 端不按套路"); [HybridRequest requestWithNative:requestModel hybridRequestSuccess:^(id responseObject) { NSDictionary *json = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableLeaves error:nil]; responseCallback([self convertToJsonData:@{@"success":@"1",@"data":json}]); } hybridRequestfail:^{ LBPLog(@"H5 call Native`s request failed"); responseCallback([self convertToJsonData:@{@"success":@"0",@"data":@""}]); }]; } }];
以上是第一阶段的平安性总结,后期应该会更新(App逆向、防重放、效劳端等)。