← Гордеев Виталий · все кейсы
Кейс · свой продукт
Cloud Delight · e-commerce · защита кода

Магазин с автовыдачей ключей и защитой от копирования

Мой магазин цифровых товаров для Minecraft: плагины, готовые сборки серверов, сайты и проекты «под ключ». Покупатель платит и сразу получает файл и лицензионный ключ. Каждый запуск плагина проверяет отдельный сервер лицензий, поэтому купленное не получится просто выложить в сеть.

25 из 25платных плагинов защищены лицензией
32товара в каталоге: сборки, плагины, сайты, проекты
9статей в SEO-блоге, почти 50 тысяч знаков
2витрины: сайт и маркет ВКонтакте, синхронизация односторонняя
Главная страница магазина Cloud Delight
cloud-delight.ruВитрина магазина. Корзина, оплата через ЮKassa, личный кабинет с покупками.
01 · Устройство

Магазин и лицензии
на разных серверах

Магазин продаёт, а ключи выдаёт и проверяет сервер лицензий. Это две разные машины, у каждой своя база.

PythonDjango 5SQLiteЮKassaJavaPaper APIMavenProGuardnginxcertbotVK API

Защита в два слоя. При сборке код плагина обфусцируется, чтобы его нельзя было прочитать и вырезать проверку. А внутри сидит страж: при запуске он сверяет ключ с сервером лицензий.

У восьми старых плагинов исходников не было, так что стража я вшиваю прямо в готовый файл. Декомпилирую главный класс, добавляю в него проверку и собираю обратно. Трогаю только этот класс, весь плагин не пересобираю.

02 · Разбор

Шесть поломок
между оплатой и ключом

В магазине цифровых товаров цепочка короткая, но каждое звено может сломаться так, что покупатель заплатит и ничего не получит.

01

Сервер лицензий отвечал «не найдено»

Симптом
Выдача ключа возвращала ошибку 404, хотя адрес был правильный.
Причина
Адрес был записан через http. Сервер перенаправлял на https, и при перенаправлении стандартная библиотека теряла POST-запрос.
Прописал сразу https и доменное имя. Выдачу ключа и подтверждение сборки проверил сквозной покупкой.
payments/license.py · дословно
def issue_license(plugin, categories, note):
    """POST на лиценз-сервер -> строка ключа. Бросает при ошибке/отсутствии конфига."""
    url = getattr(settings, "LICENSE_ISSUE_URL", "") or ""
    token = getattr(settings, "ISSUE_TOKEN", "") or ""
    if not url or not token:
        raise RuntimeError("license not configured (нет LICENSE_ISSUE_URL / ISSUE_TOKEN в .env)")
    body = json.dumps({
        "plugin": plugin,
        "categories": categories or [],
        "note": note,
    }).encode("utf-8")
    headers = {"X-Issue-Token": token, "Content-Type": "application/json"}
    # …
    req = urllib.request.Request(url, data=body, method="POST", headers=headers)
    with urllib.request.urlopen(req, timeout=20) as resp:
        data = json.loads(resp.read().decode("utf-8"))
    key = data.get("key")
    if not key:
        raise RuntimeError("лиценз-сервер не вернул key")
    return key
02

Поштучный покупатель получал нерабочий плагин

Симптом
Человек купил один плагин, а тот при запуске писал «не найден плагин-якорь» и выключался.
Причина
В конфигах шести плагинов по умолчанию был включён режим пакетной лицензии, а он нужен только внутри готовой сборки.
Теперь по умолчанию режим выключен, его включает скрипт и только при сборке пакета.
03

Старые плагины не запускались после защиты

Симптом
После вшивания стража сервер падал с ошибкой преобразования класса.
Причина
Страж компилировался новой Java, а оригиналы были собраны под Java 8, 16 и 17.
Целевая версия теперь считается по самому старому классу оригинала, а из стража я убрал новые возможности языка.
04

Заливка конфига могла стереть ключ выдачи

Симптом
На боевом сервере в файле настроек был токен выдачи, которого не было в локальной копии.
Причина
Скрипт деплоя заливал файл настроек целиком. Хватило бы одной заливки, чтобы автовыдача тихо встала, а на сайте не появилось бы ни одной ошибки.
Перед заливкой ключи сверяются: если на сервере есть настройка, которой нет локально, деплой останавливается.
05

Все страницы товаров отдавали 500

Симптом
После выкладки новой версии упали все карточки товаров.
Причина
Код уже читал варианты товара из новой таблицы, а саму таблицу на сервер ещё не создали.
Код и изменения базы выкладываю только вместе. А если таблицы вариантов нет, страница открывается без них.
06

Четыре оплаченные пустышки

Симптом
При проверке кассы нашлись товары, которые можно было оплатить, но выдать было нечего.
Причина
У четырёх позиций в каталоге не было файла выдачи. Витрина об этом не знала.
Нашёл это сквозной проверкой каталога. Все 22 ссылки выдачи живые, а позиции без файла ушли в починку до запуска рекламы на них.
03 · Правила

Как держу магазин живым

Тестировщика у маленького магазина нет, так что качество держится на правилах.

01

Всё правится на сайте

Товары, цены и описания хранятся на сайте. Маркет ВКонтакте подтягивает их оттуда, а обратно ничего не идёт.

02

Боевую базу правлю только через сверку

Скачиваю, правлю, сверяю, заливаю. Прямо в боевой базе ничего не трогаю.

03

Ничего не выдумывать о товаре

Описание сборки пишу по её конфигам и коду, из головы ничего не беру. Покупатель заметит расхождение раньше меня.

Нужен магазин цифровых товаров?

Соберу продажу, оплату и выдачу так, чтобы покупатель получал товар сразу, а вы получали деньги без ручной работы. Работаю удалённо, из Самары.

Telegram · @vitaldemort