Техническое решение для веб-антикраулера

внешний интерфейс рептилия SVG Unicode

Для контент-ориентированных компаний важна безопасность данных. Для контент-компаний важность данных невозможно переоценить. Например, если вы являетесь платформой для онлайн-обучения, данные темы очень важны, но они были украдены другими с помощью технологии сканирования? Если убрать основную конкурентоспособность, будет круто. Другой пример: независимый разработчик хочет скопировать ваш продукт, забрать ваши основные данные с помощью захвата пакетов и сканирования, а затем создать веб-сайт и приложение за короткий период времени и стать вашим сильным врагом в краткосрочной перспективе.

рептилия означает

  • В настоящее время технология сканирования напрямую находит интересующий узел на отображаемой html-странице, а затем получает соответствующий текст.
  • Некоторые веб-сайты хорошо защищены. Например, страницу списка может быть легко получить, но на странице сведений необходимо щелкнуть соответствующий элемент на странице списка, отправить itemId через форму формы, сервер генерирует соответствующие параметры и затем перенаправляет на страницу сведений (повторно после того, как направленный адрес приходит с параметром detailID страницы сведений), этот шаг может перехватить некоторых разработчиков сканеров

упражнятьсяТехническое решение для защиты от сканирования на стороне веб-сайта

С этих двух точек зрения (то, что вы видите на веб-странице, — это не то, что вы получаете, и бесполезно проверять интерфейсный запрос), я сформулировал следующий план защиты от сканирования.

  • Использовать 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

    自定义字体文件
    Вычисленный выше 1773, а то по ttf файлу страница видит 1995

  • Затем, чтобы сканеры не могли просматривать проблемы исследования JS, файлы JS шифруются. Если ваш технологический стек Vue, React и т. д., webpack предоставляет вам плагин для шифрования JS, который также очень удобен в обращении.

    Инструмент запутывания JS

  • Лично я не считаю этот метод очень безопасным. Поэтому я подумал о комбинации различных планов. Например

 Обновленная версия защиты от скалолазания

Лично я чувствую, что если дело касается опытного разработчика краулеров, вышеприведенное решение все еще может быть взломано, поэтому обновленная версия делается на основе предыдущей.

  1. Комбинация 1: файл шрифта не должен быть исправлен.Хотя запрошенная ссылка та же самая,модуль берется в соответствии с последним числом текущей метки времени.Например,модуль 4 берется в Демо, а есть 4 значения 0, 1, 2 и 3. Эти 4 значения соответствуют разным файлам шрифтов, поэтому, когда краулер ломал голову, чтобы доползти до шрифта в одном случае, он не ожидал запросить его повторно, и правила файла шрифта изменились 😂
  2. Комбинация 2: Предыдущее правило заключается в том, что проблема со шрифтом не в порядке, но нарушается только соответствие чисел. Например1 -> 4, 5 -> 8. Следующая процедура заключается в том, что каждое число соответствуетЮникод кодЗатем нужно сделать шрифт можно .ttf, .woff и так далее.

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

Такие комбинации сбивают с толку. Для обычных рептилий он заброшен.

Противозалезающие средства для апгрейда

Перечисленные выше методы в основном направлены наномерЧто мне делать, если я хочу отменить восхождение китайских иероглифов? Далее несколько вариантов

  1. план 1:Для облака слов с наибольшей частотой на вашем сайте сделайте карту китайских иероглифов, то есть файл пользовательского шрифта, шаги такие же, как и числа. Сначала создайте соответствующие файлы ttf для часто используемых китайских иероглифов; преобразуйте файлы ttf в файлы svg по приведенным ниже ссылкам, а затем выберите ранее созданные файлы svg на веб-сайте, где щелкнут ссылку «сопоставление шрифтов» ниже, и поместите файлы svg в файл svg.Сделайте сопоставление для каждого китайского символа , то есть китайский символ является специальным кодом юникода (обратите внимание, что код юникода здесь не должен быть напрямую сгенерирован онлайн, потому что напрямую сгенерированные вещи также Метод, который я даю, заключается в том, чтобы сначала использовать веб-сайт для его создания, а затем внести простое изменение в полученный результат, например, преобразовать «e342» в «e231»); затем данные, возвращаемые интерфейсом, отображаются в обратном порядке в соответствии с к правилам нашего файла шрифта.

  2. Сценарий 2:Важные шрифты веб-сайта генерируются в изображения из html-части, поэтому сканеру необходимо идентифицировать требуемый контент с большими затратами и использовать OCR. КПД тоже низкий. Таким образом, вы можете перехватить некоторые сканеры

  3. Сценарий 3:См. Обмен технологиями Ctrip «Наивысший уровень защиты от лазания — это отпечаток Canvas. Принцип заключается в том, что разные машины и разное оборудование всегда имеют ошибки на уровне пикселей в картинках, нарисованных Canvas. Поэтому мы судим, что когда большое количество холсты обращаются для доступа, всегда есть ошибки на уровне пикселей.Если отпечатки пальцев совпадают, это считается рептилией, и ее можно заблокировать».

    Я реализовал решение 1 в демо.

ключевой шаг

  1. Сначала найдите часто используемые ключевые слова в соответствии с вашими продуктами, сгенерируйтеоблако слов
  2. В соответствии с облаком слов сгенерируйте соответствующий код юникода для каждого слова.
  3. Превратите китайские иероглифы, включенные в облако слов, в библиотеку шрифтов.
  4. Сделайте библиотеку шрифтов .ttf в формате svg и загрузите ее наicomoonСоздавайте пользовательские шрифты, но с правилами, например"год"соответствующийюникод кодда">74", но нам нужно сделатьШифрование Цезаря, например, мы устанавливаемКомпенсироватьравно 1, то послеШифрование Цезаря "год"соответствующийunicodeкод">75". Используйте это правило, чтобы сделать нужную нам библиотеку шрифтов
  5. Что сервер делает каждый раз, когда вызывается интерфейс: сервер инкапсулирует метод и оценивает, находятся ли данные в облаке слов с помощью метода Если это символ в облаке слов, используйте правила (найдите код юникода соответствующий китайскому иероглифу) , а затем установить соответствующее смещение по шифрованию Цезаря, 1 в Демо, зашифровать каждый китайский иероглиф) и вернуть данные после шифрования
  6. Что делает клиент:
    • Сначала импортируйте библиотеку шрифтов китайских иероглифов, которую мы создали ранее.
    • Вызовите интерфейс, чтобы получить данные и отобразить их на соответствующем узле Dom.
    • Если это китайский текст, мы устанавливаем класс css соответствующего узла в класс китайских символов, а семейство шрифтов, соответствующее этому классу, представляет собой библиотеку шрифтов китайских символов, которую мы представили выше.
//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


Предыдущий веб-сайт ttf to svg будет ограничивать преобразование, когда файл ttf слишком велик, и позволит вам купить его.Новая ссылка будет размещена ниже.

ttf в svg

Демонстрационный адрес

效果演示

бег шаг

//客户端。先查看本机 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