资讯专栏INFORMATION COLUMN

Web 端反爬虫技术方案

MudOnTire / 449人阅读

对于内容型的公司,数据的安全性很重要。对于内容公司来说,数据的重要性不言而喻。比如你一个做在线教育的平台,题目的数据很重要吧,但是被别人通过爬虫技术全部爬走了?如果核心竞争力都被拿走了,那就是凉凉。再比说有个独立开发者想抄袭你的产品,通过抓包和爬虫手段将你核心的数据拿走,然后短期内做个网站和 App,短期内成为你的劲敌。
爬虫手段

目前爬虫技术都是从渲染好的 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 加密的插件,也很方便处理

JS混淆工具

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

 反爬升级版

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

组合拳1: 字体文件不要固定,虽然请求的链接是同一个,但是根据当前的时间戳的最后一个数字取模,比如 Demo 中对4取模,有4种值 0、1、2、3。这4种值对应不同的字体文件,所以当爬虫绞尽脑汁爬到1种情况下的字体时,没想到再次请求,字体文件的规则变掉了

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96848.html

相关文章

  • 大前端时代安全性如何做

    摘要:对于内容型的公司,数据的安全性很重要。背景目前通过中的网页分析后,我们的数据安全性做的较差,有以下几个点存在问题网站的数据通过最早期的前后端分离来实现。比如当前的日期为,那么线性变换的为,为。 之前在上家公司的时候做过一些爬虫的工作,也帮助爬虫工程师解决过一些问题。然后我写过一些文章发布到网上,之后有一些人就找我做一些爬虫的外包,内容大概是爬取小红书的用户数据和商品数据,但是我没做。我...

    andot 评论0 收藏0
  • 京JS会议分享

    摘要:本次会议的大部分资料都可在以下地址下载在上分享了京的一些内容会议主要内容为前端的相关优化以及服务器端的相关技术分享。这是这次北京之行的意外收获,号称是下一代应用的开发框架,在会议中也有不少讲师提到这个框架,现场也有出售关于这个框架的书籍。 本次会议的大部分资料都可在以下地址下载: http://vdisk.weibo.com/u/1744667943 sam_在blog上分享了京J...

    CoderStudy 评论0 收藏0
  • 阿里云前端周刊 - 第 13 期

    摘要:本文即以简单的回归拟合为例,从最基础的库安装数据导入数据预处理到模型训练模型预测介绍了如何使用进行简单的机器学习任务。 推荐 1. 京东618:ReactNative框架在京东无线端的实践 http://www.infoq.com/cn/artic... React Native最近两三年之内整个框架在业界应该说是非常热门,很多团队、大公司都在做RN的一些研究开发工作。先一起回想下在R...

    CNZPH 评论0 收藏0
  • 《Node.js设计模式》基于回调的异步控制流

    摘要:编写异步代码可能是一种不同的体验,尤其是对异步控制流而言。回调函数的准则在编写异步代码时,要记住的第一个规则是在定义回调时不要滥用闭包。为回调创建命名函数,避免使用闭包,并将中间结果作为参数传递。 本系列文章为《Node.js Design Patterns Second Edition》的原文翻译和读书笔记,在GitHub连载更新,同步翻译版链接。 欢迎关注我的专栏,之后的博文将在专...

    Chiclaim 评论0 收藏0
  • 前端开发收集 - 收藏集 - 掘金

    摘要:责编现代化的方式开发一个图片上传工具前端掘金对于图片上传,大家一定不陌生。之深入事件机制前端掘金事件绑定的方式原生的事件绑定方式有几种想必有很多朋友说种目前,在本人目前的研究中,只有两种半两种半还有半种的且听我道来。 Ajax 与数据传输 - 前端 - 掘金背景 在没有ajax之前,前端与后台传数据都是靠表单传输,使用表单的方法传输数据有一个比较大的问题就是每次提交数据都会刷新页面,用...

    ygyooo 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<