文档章节

大前端时代安全性如何做

杭城小刘
 杭城小刘
发布于 01/15 01:18
字数 4646
阅读 4360
收藏 167

之前在上家公司的时候做过一些爬虫的工作,也帮助爬虫工程师解决过一些问题。然后我写过一些文章发布到网上,之后有一些人就找我做一些爬虫的外包,内容大概是爬取小红书的用户数据和商品数据,但是我没做。我觉得对于国内的大数据公司没几家是有真正的大数据量,而是通过爬虫工程师团队不断的去各地爬取数据,因此不要以为我们的数据没价值,对于内容型的公司来说,数据是可信竞争力。那么我接下来想说的就是网络和数据的安全性问题。 对于内容型的公司,数据的安全性很重要。对于内容公司来说,数据的重要性不言而喻。比如你一个做在线教育的平台,题目的数据很重要吧,但是被别人通过爬虫技术全部爬走了?如果核心竞争力都被拿走了,那就是凉凉。再比说有个独立开发者想抄袭你的产品,通过抓包和爬虫手段将你核心的数据拿走,然后短期内做个网站和 App,短期内成为你的劲敌。

背景

目前通过 App 中的 网页分析后,我们的数据安全性做的较差,有以下几个点存在问题:

  1. 网站的数据通过最早期的前后端分离来实现。稍微学过 Web 前端的工程师都可以通过神器 Chrome 分析网站,进而爬取需要的数据。打开 「Network」就可以看到网站的所有网络请求了,哎呀,不小心我看到了什么?没错就是网站的接口信息都可以看到了。比如 “detail.json?itemId=141529859”。或者你的网站接口有些特殊的判断处理,将一些信息存储到 sessionStorage、cookie、localStorage 里面,有点前端经验的爬虫工程师心想”嘿嘿嘿,这不是在裸奔数据么“。或者有些参数是通过 JavaScript 临时通过函数生成的。问题不大,工程师也可以对网页元素进行查找,找到关键的 id、或者 css 类名,然后在 "Search“ 可以进行查找,找到对应的代码 JS 代码,点击查看代码,如果是早期前端开发模式那么代码就是裸奔的,跟开发者在自己的 IDE 里面看到的内容一样,有经验的爬虫就可以拿这个做事情,因此安全性问题亟待解决。

想知道 Chrome 更多的调试使用技巧,看看这篇文章

Chrome调试1 Chrome调试2 Chrome调试3

  1. 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 = "&#xefab;";
          }
          else if (rawNumber == 2) {
            mapData = "&#xeba3;";
          }
          else if (rawNumber == 3) {
            mapData = "&#xecfa;";
          }
          else if (rawNumber == 4) {
            mapData = "&#xedfd;";
          }
          else if (rawNumber == 5) {
            mapData = "&#xeffa;";
          }
          else if (rawNumber == 6) {
            mapData = "&#xef3a;";
          }
          else if (rawNumber == 7) {
            mapData = "&#xe6f5;";
          }
          else if (rawNumber == 8) {
            mapData = "&#xecb2;";
          }
          else if (rawNumber == 9) {
            mapData = "&#xe8ae;";
          }
          else if (rawNumber == 0) {
            mapData = "&#xe1f2;";
          }
        }
        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 加密的插件,也很方便处理

    JS混淆工具

  • 个人觉得这种方式还不是很安全。于是想到了各种方案的组合拳。比如

反爬升级版

个人觉得如果一个前端经验丰富的爬虫开发者来说,上面的方案可能还是会存在被破解的可能,所以在之前的基础上做了升级版本

  1. 组合拳1: 字体文件不要固定,虽然请求的链接是同一个,但是根据当前的时间戳的最后一个数字取模,比如 Demo 中对4取模,有4种值 0、1、2、3。这4种值对应不同的字体文件,所以当爬虫绞尽脑汁爬到1种情况下的字体时,没想到再次请求,字体文件的规则变掉了 😂
  2. 组合拳2: 前面的规则是字体问题乱序,但是只是数字匹配打乱掉。比如 1 -> 4, 5 -> 8。接下来的套路就是每个数字对应一个 unicode 码 ,然后制作自己需要的字体,可以是 .ttf、.woff 等等。

网页检察元素得到的效果 接口返回数据

这几种组合拳打下来。对于一般的爬虫就放弃了。

反爬手段再升级

上面说的方法主要是针对数字做的反爬手段,如果要对汉字进行反爬怎么办?接下来提供几种方案

  1. 方案1: 对于你站点频率最高的词云,做一个汉字映射,也就是自定义字体文件,步骤跟数字一样。先将常用的汉字生成对应的 ttf 文件;根据下面提供的链接,将 ttf 文件转换为 svg 文件,然后在下面的“字体映射”链接点进去的网站上面选择前面生成的 svg 文件,将svg文件里面的每个汉字做个映射,也就是将汉字专为 unicode 码(注意这里的 unicode 码不要去在线直接生成,因为直接生成的东西也就是有规律的。我给的做法是先用网站生成,然后将得到的结果做个简单的变化,比如将“e342”转换为 “e231”);然后接口返回的数据按照我们的这个字体文件的规则反过去映射出来。

  2. 方案2: 将网站的重要字体,将 html 部分生成图片,这样子爬虫要识别到需要的内容成本就很高了,需要用到 OCR。效率也很低。所以可以拦截掉一部分的爬虫

  3. 方案3: 看到携程的技术分享“反爬的最高境界就是 Canvas 的指纹,原理是不同的机器不同的硬件对于 Canvas 画出的图总是存在像素级别的误差,因此我们判断当对于访问来说大量的 canvas 的指纹一致的话,则认为是爬虫,则可以封掉它”。

    本人将方案1实现到 Demo 中了。

关键步骤

  1. 先根据你们的产品找到常用的关键词,生成词云
  2. 根据词云,将每个字生成对应的 unicode 码
  3. 将词云包括的汉字做成一个字体库
  4. 将字体库 .ttf 做成 svg 格式,然后上传到 icomoon 制作自定义的字体,但是有规则,比如 “年” 对应的 unicode 码“\u5e74” ,但是我们需要做一个 恺撒加密 ,比如我们设置 偏移量 为1,那么经过恺撒加密 “年”对应的 unicode 码是“\u5e75” 。利用这种规则制作我们需要的字体库
  5. 在每次调用接口的时候服务端做的事情是:服务端封装某个方法,将数据经过方法判断是不是在词云中,如果是词云中的字符,利用规则(找到汉字对应的 unicode 码,再根据凯撒加密,设置对应的偏移量,Demo 中为1,将每个汉字加密处理)加密处理后返回数据
  6. 客户端做的事情:
    • 先引入我们前面制作好的汉字字体库
    • 调用接口拿到数据,显示到对应的 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字体映射规则

实现的效果

  1. 页面上看到的数据跟审查元素看到的结果不一致
  2. 去查看接口数据跟审核元素和界面看到的三者不一致
  3. 页面每次刷新之前得出的结果更不一致
  4. 对于数字和汉字的处理手段都不一致

这几种组合拳打下来。对于一般的爬虫就放弃了。

数字反爬-网页显示效果、审查元素、接口结果情况1 数字反爬-网页显示效果、审查元素、接口结果情况2 数字反爬-网页显示效果、审查元素、接口结果情况3 数字反爬-网页显示效果、审查元素、接口结果情况4 汉字反爬-网页显示效果、审查元素、接口结果情况1 汉字反爬-网页显示效果、审查元素、接口结果情况2

<hr>

前面的 ttf 转 svg 网站当 ttf 文件太大会限制转换,让你购买,下面贴出个新的链接。

ttf转svg

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 接口的详细数据,为了做到防止抓包和无法模拟接口的情况,我们采取以下措施:

    1. 中间人盗用数据,我们可以采取 HTTPS 证书的双向认证,这样子实现的效果就是中间人在开启抓包软件分析 App 的网络请求的时候,网络会自动断掉,无法查看分析请求的情况
    2. 对于防止用户模仿我们的请求再次发起请求,我们可以采用 「防重放策略」,用户再也无法模仿我们的请求,再次去获取数据了。
    3. 对于 App 内的 H5 资源,反爬虫方案可以采用上面的解决方案,H5 内部的网络请求可以通过 Hybrid 层让 Native 的能力去完成网络请求,完成之后将数据回调给 JS。这么做的目的是往往我们的 Native 层有完善的账号体系和网络层以及良好的安全策略、鉴权体系等等。
    4. 后期会讨论 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逆向、防重放、服务端等)。

© 著作权归作者所有

共有 人打赏支持
杭城小刘
粉丝 22
博文 114
码字总数 71040
作品 0
杭州
iOS工程师
私信 提问
加载中

评论(55)

boystudio
boystudio

引用来自“boystudio”的评论

其实我是用headless模式截图输出,然后OCR处理出结果。

引用来自“杭城小刘”的评论

「模拟浏览器爬虫,抓取图片」服务端会针对具体用户做「请求阀值」。如果次数达到一定规模我就会封锁你的账号,如果需要解封,请电话联系客服人员。哈哈哈哈

引用来自“boystudio”的评论

这就不是前端的范围了,哪一端都无法防吧。不过账号可以有很多,ip代理可以有很多。

引用来自“杭城小刘”的评论

光是 IP 没什么用的,针对「账号+IP」。账号是 VIP 需要付费的,成本就高了的哈哈

引用来自“boystudio”的评论

请问哪个网站做到如此程度?后台单是黑名单或反爬虫模块都比正统业务复杂了吧,我想知道哪个网站做到如此奇葩。账号还要付费?除了三大运营商还有谁?注册个账号还要付费会有人用吗?我表示从来没见过爬不了的数据,只要你是Web。

引用来自“杭城小刘”的评论

嗯,见得少。去搜一下“https://hhb.cbi360.net”

引用来自“MK2”的评论

你网站这卖的不就是VIP服务吗?难道腾讯视频是因为防爬虫才卖VIP?

引用来自“杭城小刘”的评论

???逻辑推理能力强大到我没法接
这网站还是可以爬,只是成本变高而已,只要是给人看的,就能爬,频率等等要控制好,使用浏览器注入脚本方式,完全模拟用户使用即可。这网站本身就卖数据的,你要人家的数据自然要成本,当然他们的数据也是从别人爬来的,不如直接去源头要数据。
杭城小刘
杭城小刘

引用来自“boystudio”的评论

其实我是用headless模式截图输出,然后OCR处理出结果。

引用来自“杭城小刘”的评论

「模拟浏览器爬虫,抓取图片」服务端会针对具体用户做「请求阀值」。如果次数达到一定规模我就会封锁你的账号,如果需要解封,请电话联系客服人员。哈哈哈哈

引用来自“boystudio”的评论

这就不是前端的范围了,哪一端都无法防吧。不过账号可以有很多,ip代理可以有很多。

引用来自“杭城小刘”的评论

光是 IP 没什么用的,针对「账号+IP」。账号是 VIP 需要付费的,成本就高了的哈哈

引用来自“boystudio”的评论

请问哪个网站做到如此程度?后台单是黑名单或反爬虫模块都比正统业务复杂了吧,我想知道哪个网站做到如此奇葩。账号还要付费?除了三大运营商还有谁?注册个账号还要付费会有人用吗?我表示从来没见过爬不了的数据,只要你是Web。

引用来自“杭城小刘”的评论

嗯,见得少。去搜一下“https://hhb.cbi360.net”

引用来自“MK2”的评论

你网站这卖的不就是VIP服务吗?难道腾讯视频是因为防爬虫才卖VIP?
???逻辑推理能力强大到我没法接
M
MK2

引用来自“boystudio”的评论

其实我是用headless模式截图输出,然后OCR处理出结果。

引用来自“杭城小刘”的评论

「模拟浏览器爬虫,抓取图片」服务端会针对具体用户做「请求阀值」。如果次数达到一定规模我就会封锁你的账号,如果需要解封,请电话联系客服人员。哈哈哈哈

引用来自“boystudio”的评论

这就不是前端的范围了,哪一端都无法防吧。不过账号可以有很多,ip代理可以有很多。

引用来自“杭城小刘”的评论

光是 IP 没什么用的,针对「账号+IP」。账号是 VIP 需要付费的,成本就高了的哈哈

引用来自“boystudio”的评论

请问哪个网站做到如此程度?后台单是黑名单或反爬虫模块都比正统业务复杂了吧,我想知道哪个网站做到如此奇葩。账号还要付费?除了三大运营商还有谁?注册个账号还要付费会有人用吗?我表示从来没见过爬不了的数据,只要你是Web。

引用来自“杭城小刘”的评论

嗯,见得少。去搜一下“https://hhb.cbi360.net”
你网站这卖的不就是VIP服务吗?难道腾讯视频是因为防爬虫才卖VIP?
杭城小刘
杭城小刘

引用来自“boystudio”的评论

其实我是用headless模式截图输出,然后OCR处理出结果。

引用来自“杭城小刘”的评论

「模拟浏览器爬虫,抓取图片」服务端会针对具体用户做「请求阀值」。如果次数达到一定规模我就会封锁你的账号,如果需要解封,请电话联系客服人员。哈哈哈哈

引用来自“boystudio”的评论

这就不是前端的范围了,哪一端都无法防吧。不过账号可以有很多,ip代理可以有很多。

引用来自“杭城小刘”的评论

光是 IP 没什么用的,针对「账号+IP」。账号是 VIP 需要付费的,成本就高了的哈哈

引用来自“boystudio”的评论

请问哪个网站做到如此程度?后台单是黑名单或反爬虫模块都比正统业务复杂了吧,我想知道哪个网站做到如此奇葩。账号还要付费?除了三大运营商还有谁?注册个账号还要付费会有人用吗?我表示从来没见过爬不了的数据,只要你是Web。
嗯,见得少。去搜一下“https://hhb.cbi360.net”
boystudio
boystudio

引用来自“boystudio”的评论

其实我是用headless模式截图输出,然后OCR处理出结果。

引用来自“杭城小刘”的评论

「模拟浏览器爬虫,抓取图片」服务端会针对具体用户做「请求阀值」。如果次数达到一定规模我就会封锁你的账号,如果需要解封,请电话联系客服人员。哈哈哈哈

引用来自“boystudio”的评论

这就不是前端的范围了,哪一端都无法防吧。不过账号可以有很多,ip代理可以有很多。

引用来自“杭城小刘”的评论

光是 IP 没什么用的,针对「账号+IP」。账号是 VIP 需要付费的,成本就高了的哈哈
请问哪个网站做到如此程度?后台单是黑名单或反爬虫模块都比正统业务复杂了吧,我想知道哪个网站做到如此奇葩。账号还要付费?除了三大运营商还有谁?注册个账号还要付费会有人用吗?我表示从来没见过爬不了的数据,只要你是Web。
杭城小刘
杭城小刘

引用来自“boystudio”的评论

其实我是用headless模式截图输出,然后OCR处理出结果。

引用来自“杭城小刘”的评论

「模拟浏览器爬虫,抓取图片」服务端会针对具体用户做「请求阀值」。如果次数达到一定规模我就会封锁你的账号,如果需要解封,请电话联系客服人员。哈哈哈哈

引用来自“boystudio”的评论

这就不是前端的范围了,哪一端都无法防吧。不过账号可以有很多,ip代理可以有很多。

引用来自“杭城小刘”的评论

光是 IP 没什么用的,针对「账号+IP」。账号是 VIP 需要付费的,成本就高了的哈哈
养成爬虫团队,我自己还封装了代理池
杭城小刘
杭城小刘

引用来自“boystudio”的评论

其实我是用headless模式截图输出,然后OCR处理出结果。

引用来自“杭城小刘”的评论

「模拟浏览器爬虫,抓取图片」服务端会针对具体用户做「请求阀值」。如果次数达到一定规模我就会封锁你的账号,如果需要解封,请电话联系客服人员。哈哈哈哈

引用来自“boystudio”的评论

这就不是前端的范围了,哪一端都无法防吧。不过账号可以有很多,ip代理可以有很多。
光是 IP 没什么用的,针对「账号+IP」。账号是 VIP 需要付费的,成本就高了的哈哈
boystudio
boystudio

引用来自“boystudio”的评论

其实我是用headless模式截图输出,然后OCR处理出结果。

引用来自“杭城小刘”的评论

「模拟浏览器爬虫,抓取图片」服务端会针对具体用户做「请求阀值」。如果次数达到一定规模我就会封锁你的账号,如果需要解封,请电话联系客服人员。哈哈哈哈
这就不是前端的范围了,哪一端都无法防吧。不过账号可以有很多,ip代理可以有很多。
杭城小刘
杭城小刘

引用来自“freezingsky”的评论

为什么前端 都要加个"大"?

引用来自“杭城小刘”的评论

你觉得什么是前端???Web前端开发、Native(iOS、Android)、BFF 等概念这些东西越来越靠近前端了,所以前端不是简单的早期传统的前端开发了,而是叫做“大前端“

引用来自“freezingsky”的评论

你要不看看后端的技术栈, 但从未有人用"大"字去描述.
你要不去搜搜什么叫做「大前端」?而不是在这里拿后端在这里比较用词
f
freezingsky

引用来自“freezingsky”的评论

为什么前端 都要加个"大"?

引用来自“杭城小刘”的评论

你觉得什么是前端???Web前端开发、Native(iOS、Android)、BFF 等概念这些东西越来越靠近前端了,所以前端不是简单的早期传统的前端开发了,而是叫做“大前端“
你要不看看后端的技术栈, 但从未有人用"大"字去描述.
做前端好还是Java好?看这三方面

这几年来伴随着互联网的迅速发展,新兴互联网产业的兴起,传统行业也逐渐开始互联网化,使得互联网职业在这样的背景下成了备受瞩目的热门职业,其中“前端开发”和“Java开发”就是热门职业其...

tja8n2m2g46omtf
2017/11/14
0
0
如何深入了解Web前端的未来前景,并且用更有效率的方式去学习

随着互联网在中国的发展,只要跟互联网计算机领域相关的工作,都成了每一年的热门,从2015年开始,web前端的需要量,像火箭一样“嗖”的一下就上去了。现在各种传统行业如电子、机械、建筑等...

大数据大佬
2018/06/09
0
0
Web研发模式演变史

前不久徐飞写了一篇很好的文章:Web 应用的组件化开发。本文尝试从历史发展角度,说说各种研发模式的优劣。 一、简单明快的早期时代 可称之为 Web 1.0 时代,非常适合创业型小项目,不分前后...

ingarfield_123
2014/06/12
0
0
100offer技术meetup|聊聊“前端”的那些事儿

hi~开源中国的老朋友们,我是 100offer ,近期在上海组织了一场前端人沙龙活动,想邀请大家一起来聚聚,聊聊前端那些事儿。 为什么要做这场活动? 前端技术在近些年的变化风起云涌,很多前端...

100offer
2016/09/01
16
0
100offer技术meetup|聊聊“前端”的那些事儿

hi~开源中国社区的老朋友们,我是 100offer ,近期在上海组织了一场前端人沙龙活动,想邀请大家一起来聚聚,聊聊前端那些事儿。 为什么要做这场活动? 前端技术在近些年的变化风起云涌,很多...

100offer
2016/09/01
202
0

没有更多内容

加载失败,请刷新页面

加载更多

Httpd 整合 Tomcat 步骤

环境:Tomcat8 + Httpd2.4 工作原理:借助于Tomcat的AJP连接器实现Apache与Tomcat的通信 配置步骤: 1. 配置httpd.conf 新增: Include conf/extra/mod_jk.conf 修改:添加 index.jsp <IfM...

ZeroneLove
昨天
1
0
Docker笔记3——容器命令(未写完,明天整理接着写)

未写完,明天整理接着写 新建并启动容器 docker run docker run [OPTIONS] IMAGE [COMMEND] [ARG...] OPTIONS: --name=[容器新名字] :为容器指定一个名称 -d:后台运行容器,并返回容器ID,...

HappyBKs
昨天
1
0
2018个人年终总结

感谢领导的信任和指导,新的一年获得了很多成长和提高,改掉了很多不好的习惯。 在这一年里,我在领导的帮助下,主要完成了以下功能: 1、完成上海银行版本投资营销相关功能的开发。 2、完成车...

万山红遍
昨天
11
0
保密工作与linux系统的发展

保密工作从性质上可以分成商业方面的保密和国家安全方面的保密。由于自己从事的是IT方面的工作,工作中必然会接触涉及到计算机信息方面的相关文件。加上单位已近通过武器装备科研生产单位二级...

linux-tao
昨天
3
0
Spark共享变量

概述 Spark程序的大部分操作都是RDD操作,通过传入函数给RDD操作函数来计算。这些函数在不同的节点上并发执行,但每个内部的变量有不同的作用域,不能相互访问,所以有时会不太方便,Spark提...

仟昭
昨天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部