以前在上家公司的时候作过一些爬虫的工做,也帮助爬虫工程师解决过一些问题。而后我写过一些文章发布到网上,以后有一些人就找我作一些爬虫的外包,内容大概是爬取小红书的用户数据和商品数据,可是我没作。我以为对于国内的大数据公司没几家是有真正的大数据量,而是经过爬虫工程师团队不断的去各地爬取数据,所以不要觉得咱们的数据没价值,对于内容型的公司来讲,数据是可信竞争力。那么我接下来想说的就是网络和数据的安全性问题。
对于内容型的公司,数据的安全性很重要。对于内容公司来讲,数据的重要性不言而喻。好比你一个作在线教育的平台,题目的数据很重要吧,可是被别人经过爬虫技术所有爬走了?若是核心竞争力都被拿走了,那就是凉凉。再比说有个独立开发者想抄袭你的产品,经过抓包和爬虫手段将你核心的数据拿走,而后短时间内作个网站和 App,短时间内成为你的劲敌。
目前经过 App 中的 网页分析后,咱们的数据安全性作的较差,有如下几个点存在问题:javascript
想知道 Chrome 更多的调试使用技巧,看看这篇文章css
想知道 Charles 的更多使用技巧,能够看看这篇文章html
本人从这2个角度(网页所见非所得、查接口请求没用)出发,制定了下面的反爬方案。前端
# 好比须要正确显示的数据为“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 为例讲解后端须要作的事情java
将生成数据转换成 JSON 返回给调用者node
// 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(); };
前端根据服务端返回的数据逆向解密webpack
$("#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,根据咱们约定的算法,能够的到结果为1773git
JS混淆工具github
我的以为若是一个前端经验丰富的爬虫开发者来讲,上面的方案可能仍是会存在被破解的可能,因此在以前的基础上作了升级版本web
这几种组合拳打下来。对于通常的爬虫就放弃了。
上面说的方法主要是针对数字作的反爬手段,若是要对汉字进行反爬怎么办?接下来提供几种方案
本人将方案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"; }
这几种组合拳打下来。对于通常的爬虫就放弃了。
前面的 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
目前 App 的网络通讯基本都是用 HTTPS 的服务,可是随便一个抓包工具都是能够看到 HTTPS 接口的详细数据,为了作到防止抓包和没法模拟接口的状况,咱们采起如下措施:
关于 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逆向、防重放、服务端等)。