Исходные данные
Итак, предположим у нас есть на фронте React.js, на бэке соответственно DRF. Либо другие аналоги. API бэкенда полностью открыто - как для нашего фронта, так и открыто для postman, scrapy и т.п. Также у нас есть информация, что используя наше же api - конкуренты активно парсят цены, остатки и т.п. Можем ли мы им это запретить? - Не думаю. А вот усложнить им жизнь и развлечься за деньги заказчика сделать это интересным образом - вполне.
Поиск решения
Сразу возникает мысль воткнуть на фронт какой-то токен и дальше со стороны сервера проверять его наличие. Однако разрабы конкурентов вполне способны нажать F12 на сайте, найти этот токен в куках или заголовках запросов и добавить к себе в код парсера - который точно также будет передавать этот же токен.
Вот если бы токен менялся сам по себе постоянно. Что же у нас меняется само по себе?
Так это же время!
Возможно же передавать с фронта время в которое был сделан запрос, а на бэкенде проверять время - когда этот запрос был получен. Кто - то скажет:
- А как же часовые пояса?
А я отвечу:
- Будем использовать timestamp.
Соответственно если в куках/загловках нам приходит с фронта, что запрос был сделан больше чем +-10 минут - значит что-то тут нечисто. Время 10 минут выбрано чисто для примера, думаю вполне можно использовать и 5 минут и 3 минуты и т.д.
Единственная проблема остается: разрабы конкурентов могут модифицировать свой парсер - чтобы он точно также ставил время запроса.
И тут нам приходит на помощь шифрование. Будем использовать симметричное шифрование.
Если мы будем использовать зашифрованный timestamp - который будет генерироваться на фронте реакта, а дешифроваться на бэкенде - то это вполне нас устроит.
Глянем что там у нас есть из существующих алгоритмов. Зашифруем с помощью них пару близких к друг другу timestamp
-
AES
1640785027 => U2FsdGVkX19OVF5IzcrdnGxJIlenezRUNeqyGuCcHU0=
1640785140 => U2FsdGVkX19JULnlP8u/ui6LcLYXBW3txJgHNL183DM=
Как мы видим из-за довольной близости значений есть общая часть и можно понять, что значения слабо меняются от времени да и взглянув на код можно будет понять - что за агоритм используется.
В общем было бы здорово, если бы от изменения считай 1- 2 секунд значение выглядело абсолютно по-другому. Да и учитывая, что код будет по шифрованию на фронте, понятно что минифицированный и обфусцированный. Защита алгоритмом еще никому не помешала.
Что-то получилось?
Пример зашифрованных timestamp
Получилось, достаточно чтобы сбить столку и вывести из строя парсеры конкурентов
1640785595 => 171.148.245.238.228
1640785608 => 216.129.188.192.174
В timestamp быстро меняется самая правая цифра, чем цифра ближе к левой части - тем она реже меняется. Значит мы должны придумать такой алгоритм шифрования - чтобы изменения секунд влияло на весь остальной результат.
Мы можем представить наш timestamp - в виде
16 |
40 |
78 |
55 |
95 |
Где каждая ячейка может принимать значения [00; 99]. Понятно, что в первой ячейке уже всегда будут значения >= 16 - но это никак не повлияет на нас.
Когда я взглянул на эти цифры - чем-то они напомнили мне коды символов. И это как раз то что нам нужно. Сгенерируем таблицу шифрования, где ключи в диапазоне [00; 99], а значения уникальные символы. Таблица дешифрования получается - если в таблица шифрования менять местами ключи с значениями.
from random import randint
def generate_decode_table():
decode_table = {}
for i in range(100):
symbol = chr(randint(128, 254))
while symbol in decode_table:
symbol = chr(randint(128, 254))
decode_table[symbol] = str(i) if i > 9 else f'0{i}'
return decode_table
decode_table = generate_decode_table()
encode_table = {v: k for k, v in decode_table.items()}
Получим таблицу для фронта вида:
let encodeTable = {"00":"ú","01":"ï","02":"", ... '98': '®', '99': '£'};
И обратную таблицу для дешифровки на бэкенде вида:
decode_table = {'£': '99', ... 'ú': '00', 'û': '78', 'ü': '15','ý': '66'}
Чтобы изменения секунд влияли на весь шифр начнем шифровать именно с них. Развернем нашу строку:
95 |
55 |
78 |
40 |
16 |
Будем шифровать ячейки начиная с 95
. На это значение влияет только оно само.
Ищем его в шифровочной таблице - получаем '«'
Получаем код символа 171. Это первое число нашего шифра.
Далее шифруем 55
. На это значение также влиет пришлое 95
. Суммируем эти числа:
55 + 95 = 150
. Наша таблица шифрования ограниченная значениями [00;99]. Поэтому возьмем остаток от деления 150 % 100 = 50
в таблице шифрования это '\x94'
код это символа 148
.
Далее по аналогии шифруем оставшиеся числа. Единственное учитываем момент, что после остатка от деления может быть число < 10 и тогда нужно будет добавать впереди символ нуля, например '09'
function generateEncodedTimestamp() {
let ts = Math.floor(Date.now() / 1000).toString();
let pairsToEncode = [parseInt(ts.slice(8, 10)), parseInt(ts.slice(6, 8)), parseInt(ts.slice(4, 6)), parseInt(ts.slice(2, 4)), parseInt(ts.slice(0, 2))];
let encodedPairs = [];
pairsToEncode.forEach((el, i) => {
let encodedSum = pairsToEncode.slice(0, i).reduce((a, b) => a + b, 0);
let keyForTable = ((el + encodedSum) % 100).toString();
keyForTable = keyForTable.length > 1 ? keyForTable : `0${keyForTable}`;
encodedPairs.push(encodeTable[keyForTable].charCodeAt(0));
});
return encodedPairs.join('.');
}
Этот зашифрованный timestamp вида 171.148.245.238.228
Можно добавить в заговолок каждого запроса к примеру. При каждом обновлении страницы данный токен очень сильно отличается и кажется на первый взгляд, что в нем нет логики и он просто случайный.
На бэкенде же дешифрование выполняется в обратном порядке.
Достаем токен из заголовком к примеру
Получаем символ по коду с помощью
chr
Если в таблице дешифрования такого символа нет - то это парсер
Достаем символу в таблице число и вычитаем от него предыдущую сумму. Не забываем обработать случай, если сумма больше данного числа
Берем остаток от деления на 100, добавляем символ нуля в начале при необходимости.
Все соединяем, разворачиваем и у нас получился timestamp от фронта
POSSIBLE_DIFF_EPOCH = 300
def is_robot():
seconds_from_epoch_server = int(time.time())
# client_secret = self.request.headers.get('X-VERSION')
client_secret = '172.154.130.251.208'
if client_secret is None:
return True
symbols_for_codes = [chr(int(el)) for el in client_secret.split('.')]
reverse_result = []
for index, symbol in enumerate(symbols_for_codes):
try:
encoded_pair = decode_table[symbol]
except KeyError:
return True
if index == 0:
reverse_result.append(encoded_pair)
continue
previous_sum = sum([int(el) for el in reverse_result])
int_encoded_pair = int(encoded_pair)
if int_encoded_pair < previous_sum:
sum_before_division = (previous_sum // 100 + 1) * 100 + int_encoded_pair
else:
sum_before_division = int_encoded_pair
current_pair = str((sum_before_division - previous_sum) % 100)
if len(current_pair) < 2:
current_pair = f'0{current_pair}'
reverse_result.append(current_pair)
seconds_from_epoch_client = int(''.join(reversed(reverse_result)))
if abs(seconds_from_epoch_server - seconds_from_epoch_client) > POSSIBLE_DIFF_EPOCH:
return True
return False
Ну а дальше когда знаем парсер это или нет можно как просто возвращать какую-то ерунду, так и добавить к ценам к примеру некий коэффициент.
Главное код на фронте хорошенько запутать. Благо сейчас достаточно сервисов по обфускации. А даже если код прогнать обратно и деобфускацировать - то не зная алгоритма с ходу маловероятно разобраться как генерируются токен.
Быть может кому-то будет полезен мой подход и предложенный вариант реализации.
Комментарии (47)
tuxi
02.01.2022 15:14+7Лет 5 уже «развлекаюсь» аналогичным образом, с поправкой на то, что приходится осуществлять защиту всего паблик фронта, а не ендпоинтов API.
Один из вариантов дальнейшего развития событий: часть скраперов, которых заказчик парсинга ткнет носом в то, что в сданной ими работе на 80..90% фейковые цены или данные, начнут злобно и тупо атаковать другие ендпоинты системы, создавая нагрузку.
И вот на этом моменте, надо будет иметь под рукой некую готовую к проду систему, которая начнет собирать список их IP из прокси листов, для последующего использования этого списка в целях защиты всего проекта.TerionGVS5 Автор
02.01.2022 16:01К сожалению все эти прокси IP стоят дешево и их хоть сотнями можно покупать. =(
Bonio
02.01.2022 16:11Листы проксей можно найти в открытых источниках. Не 100%, но большую часть вредоносных запросов можно с их помощью перекрыть.
tuxi
02.01.2022 16:20Да пусть покупают. Тут смысл в том, что если в дальнейшем, например в течении последующих 2..3..4 недель приходят запросы, которые по поведенческому параметру похожи на скраппинг, то наличие таких айпи в том списке, увеличивает вес решения о признании запроса «не несущим профитной составляющей для проекта».
kubrack
02.01.2022 15:19+2добавить к ценам к примеру некий коеффциент.
отличный сюрприз для ваших клиентов, оставляющих открытые страницы со сделаным выбором на потом
Bonio
02.01.2022 15:28+1Так токен генерируется из javascript в момент запроса. Хоть на неделю пусть оставляют.
kubrack
02.01.2022 16:03+2А, ну да, написал не подумавши, сори, спасибо.
Но все равно это как-то… by obscurity.
Ведь все равно этот код отдается на клиент, который может, даже не де-обфусцируя его, просто запускать у себя каждый раз для получения этого поля в парсер.Может глупость скажу опять как не фронтендер, но почему не
- спроектировать фронт так, чтобы он не перерисовывался по каждому чекбоксу, беся клиентов (привет, Розетка), а, например показывал кнопочку "применить" (во многих магазинах так сделано)
- каждому запросу давать открытый одноразовый ключ
- придерживать каждый последующий ответ на конкретный запрос (отслеживая по п. 2) на 2-4 секунды (для правильного клиента их не нужно будет чаще т к п. 1). И пусть парсят по ~1000 позиций в час.
- ну и в bulk запросах ограничить макс. кол-во результатов разумным числом.
PaulIsh
02.01.2022 16:19По п 2. Инициатор запроса фронт, значит чтобы каждому запросу дать свой одноразовый ключ его должен сформировать фронт. Значит можно фронт разобрать и алгоритм достать.
По п.3 обычно ставят пакеты (rate limit), которые ограничивают количество запросов с ip в единицу времени.
kubrack
02.01.2022 16:28С ІР нельзя, до сих пор есть много мест где сотни людей за одним ІР. А ключ — просто строка одноразовая от бека, там нечего разбирать и доставать. Можно просто куку если GDPR заморочки уже решены для других кук. Именно чтобы трекать клиента, но не по ІР
Bonio
02.01.2022 16:54А смысл такого ограничения? Поменять строку с кукой на что-то рандомное в парсере, что может быть проще?
kubrack
02.01.2022 16:58Смысл в рейт лимите для каждого реального клиента, не по ір
Bonio
02.01.2022 17:00+1А как новго реального клиента отличать? Клиент без кук реальный? А с рандомным мусором в куках?
kubrack
02.01.2022 17:44+1Ну например как клиент, не получивший еще куки, не должен на эндпоинты с данными вообще приходить. А раздачу кук рейтлимитить по IP, жертвуя тем, что первый запрос будет иногда чуть тупить для клиентов, которые сидят толпой на одном ІР.
PaulIsh
02.01.2022 18:35Вы ушли в нюансы, а всё равно скатились к IP. Конечно, не обязательно лимитировать совсем все запросы (хотя лучше все, но поставить лимит адекватный), можете лимитировать которые выдают токен, а дальше ограничивать доступ к api по токену (ну или куки в вашем варианте).
dopusteam
02.01.2022 15:26+3симмитричное
минифицрованный
обфускацированный
повляет
коеффциент
развлечсязаказщикаИзвините, что не ctrl+enter, но почему бы не проверить перед публикацией?
PaulIsh
02.01.2022 15:26+5Всё равно, если алгоритм на фронте, то достанут и разберут. Может есть ещё варианты как усложнить использование api сторонними инструментами? Webassembly не может помочь спрятать алгоритм?
TerionGVS5 Автор
02.01.2022 16:05+3Webassembly - не трогал. Думаю им проще будет парсер переписать на selenium - чем разбираться. Ну в любом случае - пока заметят, что их парсер некорректно работает, пока перепишут, особенно - если с selenium раньше не работали. Каких-то проблем даже такая защита способна доставить)
nik_the_spirit
02.01.2022 15:30+9Кажется, вы переизобрели CSRF-токен.
TerionGVS5 Автор
02.01.2022 16:00Как правило csrf используют для запросов - которые что-то изменяют. Втыкать его во все запросы в том числе простые GET - не хотелось.
SergeiMinaev
02.01.2022 15:41+5Выше уже подметили про CSRF. Ну и это вам только так кажется, что никто не догадается, что вы там задумали. На деле, очень быстро догадаются. Проверено на личном опыте. К тому же, если будут парсить через selenium, то от этого всего не будет толку. Для себя нашёл единственный более-менее работающий вариант: детектить бота по поведению и показывать капчу.
glader
02.01.2022 17:11+1AcckiyGerman
02.01.2022 23:49Как-то в году еще 2003 пытался написать бота для популярной браузерной игрушки (fight club или чтото такое) и наткнулся на каптчу, и мне точно такая же идея пришла в голову - нанимать живых людей для решения капчи. Правда я был ленивым студентом, и далее идеи ничего делать не стал.
Andrey_Dolg
02.01.2022 15:42Главное случайно не заблокировать фрон для некоторых из клиентов пока играешься. А касательно парсеров, если информация того стоит(обычно нет) разберутся и спарсят, вопрос времени. Если защита будет сложной, то тот кто разберётся просто будет предоставлять к ней доступ за деньги вот и всё.
Так же вместо использования api могут начать искать уязвимости, что может быть и неплохо если заметить.
AjnaGame
02.01.2022 16:07+2Куда лучше уже рейты ставить т ротлинг. А Ваше решение попусту потраченное время ну и немерянно наивности)
Bonio
02.01.2022 16:10Абсолютной защиты не сделать. Троттлинг и рейт-лимиты тоже не будут работать, если парсер будет работать по спискам проксей.
А вот усложнить жизнь парсерам в какой-то степени можно, почему бы это не сделать?AjnaGame
02.01.2022 16:17+1Верно, нельзя. Но троттлинг эффективнее решения автора. Плюс, списки проксей тоже можно мониторить и блеклистить, но это уже целый отдельный проект)
vlreshet
02.01.2022 17:07+5Я что-то упускаю, или почему нельзя сделать запрос на «настоящую страницу», взять с неё эту куку, и потом уже пульнуть по API? Между этими действиями пройдёт минимум времени, бек ничего не заподозрит. Другими словами — «одноразовость» куки ведь не обеспечивается. Таким образом можно удвоить нагрузку себе на сервер, а парсеры при этом надо будет переписать по-минимуму
Akuma
02.01.2022 17:30+9Как владелец парсера, скажу, что вы ничего толком не усложнили :)
Проблем вы доставите только если будете инфо о товаре выводить картинкой, чтобы не было доступа к текстовой информации. И менять расположение элементов на этой картинке с каждым запросом. Распознавать фото, да еще и меняющееся, сложно и затратно.
А эта "защита" ничего не стоит, поскольку ваш код для создания токена все так же на клиенте. Банально смотрим в дев-панели откуда был вызван запрос, смотрим исходники и просто копируем себе функцию генерации токена, даже разбирать ее не надо.
Либо запускаем puppet и просто ходим по страницам. Вообще ничего не надо делать.
Если хотите усложнить жизнь парсерам, лучше ставьте защиту от ботов на уровне всех запросов. Тот же Cloudflare отлично справляется, но есть и платные шикарные варианты.
P.S. А вот реальным клиентам это все доставит проблем.
mixsture
02.01.2022 17:51+2Проблем вы доставите только если будете инфо о товаре выводить картинкой, чтобы не было доступа к текстовой информации.
Кстати, один из вариантов у нас недавно ЦИК изобрел. Помните уникальные шрифты для каждой ячейки таблицы? :)
Для человеческого глаза это незаметно, но заставило тессеракт прикручивать для анализа.
Neikist
02.01.2022 18:39+5Проблем вы доставите только если будете инфо о товаре выводить картинкой, чтобы не было доступа к текстовой информации.
Ух как порадуются слепые/слабовидящие пользователи смотрящие сайт через скринридеры)
mixsture
02.01.2022 17:49+1Я бы подошел к этой защите немного с другой стороны: писать бота относительно легко, когда легко видно разграничение — пустило/не пустило. Так вот если «не пустило» будет крайне похоже на «пустило», то стоимость поддержки бота вырастет на порядки.
Например, на «не пустило» выдавать реальные данные, но примешивать 20% неправильных цен, 20% перемешанных описаний к товарам.VadimChin
02.01.2022 20:08+1ага, если гипермаркет или поставщик для гипермаркетов и руки "мешателей" вдруг оказались не очень прямыми.
если парсинг открытых данных так сильно влияет на бизнес - у меня плохие новости.tuxi
02.01.2022 21:01Вы даже не представляете себе какой процент этих «парсеров» написаны в стиле «я дятел, и все что вокруг меня — это деревья, которые надо долбить каждые 2 секунды».
Ни разу не встречали сайты-копии, контент для которых берется прямо вот онлайн с атакуемого сайта? Никакого промежуточного хранения данных, никакого кеширования хотя бы на 10 минут. Вот прям долбят по списку урлов каждый урл 1 раз в 5 секунд.
Я вот регулярно такое встречаю.sden77
02.01.2022 22:08от такого хорошо помогает упомянутая выше защита от ботов с помощью CloudFlare и ему подобных. IP датацентров по умолчанию считаются подозрительными и им чаще выдаётся капча или устанавливается очень низкий порог блокировки
alexander222
02.01.2022 18:26+2для защиты от скрапинга сойдёт наверно, но иногда люди начинают изобретать свои
велосипедыалгоритмы для более критичных вещей. И если они не профессиональные математики и специалисты по криптографии, то кончается это обычно не очень хорошо.
dopusteam
А почему просто авторизацию по какому нибудь jwt не прикрутить?
Kazzman
Так пытаются свой фронт от чужого отличать
TerionGVS5 Автор
jwt - это классика. И чуть модернизировать парсер, чтобы получить jwt - не вижу в этом больших сложностей. Обхитрить то, что я описал - займет больше времени.
dopusteam
Совсем не больше. Вам уже предложили несколько вариантов обхода. В общем случае, если нужно, то обойти такую "защиту" - дело порядка часа.