Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Исходные данные
Итак, предположим у нас есть на фронте 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
Ну а дальше когда знаем парсер это или нет можно как просто возвращать какую-то ерунду, так и добавить к ценам к примеру некий коеффциент.
Главное код на фронте хорошенько запутать. Благо сейчас достаточно сервисов по обфускации. А даже если код прогнать обратно и деобфускацировать - то не зная алгоритма с ходу маловероятно разобраться как генерируются токен.
Быть может кому-то будет полезен мой подход и предложенный вариант реализации.