资讯专栏INFORMATION COLUMN

大前端时代安全性如何做

andot / 1641人阅读

摘要:对于内容型的公司,数据的安全性很重要。背景目前通过中的网页分析后,我们的数据安全性做的较差,有以下几个点存在问题网站的数据通过最早期的前后端分离来实现。比如当前的日期为,那么线性变换的为,为。

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

JS混淆工具

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

反爬升级版

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

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

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

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

相关文章

  • 《从零构建前后分离web项目》:开篇 - 纵观WEB历史演变

    摘要:更详细的内容下一章开篇深入聊聊前后分离讲述关于我目前在写从零构建前后分离项目系列,修正和补充以此为准不断更新的项目实践地址彩蛋提前预览下一章传送门 开篇 : 纵观WEB历史演变 在校学习和几年工作工作中不知不觉经历了一半的 WEB 历史演变、对近几年的发展比较了解,结合经验聊聊 WEB 发展历史。 演变不易,但也是必然,因为为人始终要进步。 WEB 的发展史 一、开山鼻祖 - 石器时代...

    tracy 评论0 收藏0
  • 《从零构建前后分离web项目》:开篇 - 纵观WEB历史演变

    摘要:更详细的内容下一章开篇深入聊聊前后分离讲述关于我目前在写从零构建前后分离项目系列,修正和补充以此为准不断更新的项目实践地址彩蛋提前预览下一章传送门 开篇 : 纵观WEB历史演变 在校学习和几年工作工作中不知不觉经历了一半的 WEB 历史演变、对近几年的发展比较了解,结合经验聊聊 WEB 发展历史。 演变不易,但也是必然,因为为人始终要进步。 WEB 的发展史 一、开山鼻祖 - 石器时代...

    songjz 评论0 收藏0
  • Codes Don't Lie —— Integ

    摘要:大局意识就是合理地优先处理某些重要的小项目。对于所做事情的意义,我想到了美国西部大淘金的时代。我在美国时发现很多产业都只剩下几个巨头在竞争,而国内各种类似领域有大量小公司的存在。 Integ 是 SegmentFault 的前端 Hacker,在本次访谈中贡献了编程五年多的感悟与总结。Codes Dont Lie 这个标题(并非 Hips Dont Lie)代表了 Integ 的诚恳和...

    LinkedME2016 评论0 收藏0
  • 鸟瞰前端 , 再论性能优化

    摘要:前端性能优化的涉及点从服务器到协议再到宿主环境本身都要有比较深刻的认识,业界目前主要还是以雅虎总结出来条前端性能优化的黄金军规为参考。 欢迎大家前往腾讯云技术社区,获取更多腾讯海量技术实践干货哦~ 导语 : 从事前端有6年+的时间了,从最开始的美工到重构再到偏向js逻辑开发的前端开发,一直在前端这个行业里面摸索和学习,我现在将自己这些年的一个心得体会来个系统性的梳理写成一篇关于性能优化...

    voidking 评论0 收藏0

发表评论

0条评论

andot

|高级讲师

TA的文章

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