Eröffnung in Kürze — Autoren behalten 100 % ihres Preises Autor werden Anmelden Konto erstellen
Hallo, anmelden Konto & Listen IhrAutorenbereich

Die Integration, Umgebung für Umgebung

1.Ihre Bibliothek ist die von niemandem sonst

Die Datei, die Sie herunterladen, wird für ein Produkt erzeugt. Klasse, Dateiname und Cache-Datei tragen alle einen aus diesem Produkt abgeleiteten Namen, und die aufgerufene Adresse wird zur Laufzeit zusammengesetzt statt als eine Zeichenkette geschrieben. Zwei Ihrer eigenen Produkte teilen keinen einzigen Bezeichner.

Seien wir klar, was das bringt. Wer Ihre Dateien öffnet und liest, findet die Prüfung in zwei Minuten — das hat sich nicht geändert und wird sich nie ändern. Was es stoppt, ist das andere: ein Skript, das in Tausenden Produkten nach einem bekannten Klassennamen sucht und ihn automatisch entfernt. So entstehen genullte Kopien in Serie, und das funktioniert hier nicht mehr. Die Kosten, Ihr Produkt zu raubkopieren, steigen von null auf eine manuelle Durchsicht, Produkt für Produkt.

Die Beispiele auf dieser Seite nennen die Klasse Licence. In Ihrem Download trägt sie ihren echten Namen, angezeigt neben dem Download-Button in Ihrem Autorenbereich.

2.Die Integration, Umgebung für Umgebung

Öffnen Sie den Block, der zu Ihrem Produkt passt. Jeder ist vollständig und zum Einfügen bereit; benennen Sie monmodule um und passen Sie die Ablage des Schlüssels an Ihre eigenen Einstellungen an. Die PHP-Umgebungen nutzen die herunterladbare Bibliothek; die anderen zeigen den kompletten Client, samt Cache und Signaturprüfung.

PHP PHP ohne Framework SDK PHP 5.6 → 8.4

Die kürzeste Form. Setzen Sie die Prüfung an den Eingang dessen, was Sie schützen — einen Front-Controller, eine Verwaltungsseite, ein geplantes Skript.

myproduct.php
require_once __DIR__ . '/Licence.php';

$licence = new Licence(
    $config['licence'],
    __DIR__ . '/cache'
);

if (!$licence->valide()) {
    http_response_code(403);
    exit($licence->message());
}
PS PrestaShop 1.7 und 8 SDK 1.7 · 8 · 9

Halten Sie eine einzige Instanz für das ganze Modul. Prüfen Sie in getContent(), damit der Händler sieht, warum der Konfigurationsbildschirm ablehnt, und ebenso in Ihren Anzeige-Hooks, damit ein halb eingerichtetes Modul im Shop nichts ausgibt.

mymodule.php
require_once dirname(__FILE__) . '/Licence.php';

class MonModule extends Module
{
    private $licence = null;

    private function licence()
    {
        if ($this->licence === null) {
            $this->licence = new Licence(
                Configuration::get('MONMODULE_LICENCE'),
                _PS_CACHE_DIR_ . 'monmodule'
            );
        }
        return $this->licence;
    }

    public function getContent()
    {
        if (Tools::isSubmit('submitLicence')) {
            Configuration::updateValue('MONMODULE_LICENCE', Tools::getValue('licence'));
        }

        $l = $this->licence();

        if (!$l->valide()) {
            return $this->displayError($l->message()) . $this->formulaireLicence();
        }
        if ($l->essai()) {
            return $this->displayWarning($this->l('Development licence')) . $this->contenu();
        }

        return $this->contenu();
    }

    public function hookDisplayHeader()
    {
        if (!$this->licence()->valide()) {
            return '';
        }
        return $this->contenuFront();
    }
}

Geprüft gegen 1.7, 8 und 9. Die verwendeten Aufrufe — Module, Configuration, Tools, _PS_CACHE_DIR_ — haben die Entfernungen in 9.0 unberührt überstanden. Die für 10.0 angekündigten Deprecations betreffen die Admin-Controller, die dieses Beispiel nicht benutzt.

WP WordPress SDK 5.0 → 7.1

Eine statische Instanz in einer Funktion, ein Hinweis in der Verwaltung und der Rumpf des Plugins hinter der Prüfung. Führen Sie die Prüfung nicht im Frontend jeder Seite aus — der Cache macht sie billig, aber ein Hinweis gehört in die Verwaltung.

myplugin.php
require_once __DIR__ . '/Licence.php';

function monplug_licence() {
    static $l = null;
    if ($l === null) {
        $l = new Licence(
            get_option('monplug_licence'),
            WP_CONTENT_DIR . '/cache/monplug'
        );
    }
    return $l;
}

add_action('admin_notices', function () {
    $l = monplug_licence();
    if (!$l->valide()) {
        printf('<div class="notice notice-error"><p>%s</p></div>', esc_html($l->message()));
    } elseif ($l->essai()) {
        echo '<div class="notice notice-warning"><p>'
           . esc_html__('Development licence', 'monplug') . '</p></div>';
    }
});

add_action('init', function () {
    if (monplug_licence()->valide()) {
        monplug_demarrer();
    }
});

Die fünf verwendeten Aufrufe — add_action, add_filter, get_option, esc_html, WP_CONTENT_DIR — haben sich seit WordPress 5.0 nicht geändert und funktionieren weiterhin auf dem 7.1-Zweig. Nichts hier hängt vom Block-Editor oder von einer REST-Route ab.

Bei WooCommerce sollten Sie das absichern, was Sie dem Shop hinzufügen, nicht das Plugin als Ganzes: ein Zahlungsweg, der verschwindet, ist für den Händler klarer als ein Plugin, das still stirbt.

woocommerce.php
add_filter('woocommerce_payment_gateways', function ($passerelles) {
    if (monplug_licence()->valide()) {
        $passerelles[] = 'WC_Gateway_MonPlug';
    }
    return $passerelles;
});

add_action('woocommerce_admin_field_monplug_licence', function () {
    $l = monplug_licence();
    if (!$l->valide()) {
        echo '<div class="error inline"><p>' . esc_html($l->message()) . '</p></div>';
    }
});
M2 Magento 2 SDK 2.4

Verpacken Sie die Bibliothek in ein Model und injizieren Sie es, wo Sie es brauchen. Das Cache-Verzeichnis muss das von Magento sein, sonst löscht ein Deployment Ihr Urteil bei jeder Veröffentlichung.

Model/License.php
namespace Editeur\MonModule\Model;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Filesystem\DirectoryList;

class Licence
{
    private $licence;

    public function __construct(ScopeConfigInterface $config, DirectoryList $dossiers)
    {
        $this->licence = new \Licence(
            (string) $config->getValue('monmodule/general/licence'),
            $dossiers->getPath(DirectoryList::CACHE) . '/monmodule'
        );
    }

    public function valide(): bool
    {
        return $this->licence->valide();
    }

    public function message(): string
    {
        return $this->licence->message();
    }
}

Die Bibliothek hat keinen Namensraum: Deklarieren Sie sie in der composer.json Ihres Moduls, statt require_once von Hand aufzurufen.

composer.json
{
  "autoload": {
    "files": [
      "Licence.php"
    ]
  }
}
LV Laravel SDK 9 → 12

Eine Middleware ist der richtige Ort: Registrieren Sie sie auf der Routengruppe, die zu Ihrem Produkt gehört, nie global, damit ein Lizenzproblem nie den Rest der Anwendung mitreißt.

app/License/CheckLicense.php
namespace App\Licence;

use Closure;
use Illuminate\Http\Request;

class VerifieLicence
{
    private $licence;

    public function __construct()
    {
        $this->licence = new \Licence(
            config('monmodule.licence'),
            storage_path('app/monmodule')
        );
    }

    public function handle(Request $requete, Closure $suivant)
    {
        if (!$this->licence->valide()) {
            abort(403, $this->licence->message());
        }

        return $suivant($requete);
    }
}
SF Symfony SDK 5.3 → 7

Ein Event-Subscriber auf kernel.request, der Sub-Requests überspringt. Übergeben Sie Schlüssel und Cache-Verzeichnis als Argumente in Ihrer Service-Definition, statt die Konfiguration in der Klasse zu lesen.

src/License/LicenseSubscriber.php
namespace App\Licence;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;

class LicenceSubscriber implements EventSubscriberInterface
{
    private $licence;

    public function __construct(string $cle, string $dossierCache)
    {
        $this->licence = new \Licence($cle, $dossierCache);
    }

    public function onKernelRequest(RequestEvent $evenement): void
    {
        if (!$evenement->isMainRequest()) {
            return;
        }

        if (!$this->licence->valide()) {
            throw new AccessDeniedHttpException($this->licence->message());
        }
    }

    public static function getSubscribedEvents(): array
    {
        return [KernelEvents::REQUEST => ['onKernelRequest', 16]];
    }
}

isMainRequest() kam mit Symfony 5.3. Rufen Sie auf einer älteren Version stattdessen isMasterRequest() auf — alles andere ist identisch.

JS Node.js API Node 18+

Keine Abhängigkeit über die Standardbibliothek hinaus. Das ist der vollständige Client: signierte Antwort, an seinen Host gebundener Cache, 24 Stunden Wiederverwendung und 30 Tage Schonfrist — dieselben Regeln wie in der PHP-Bibliothek.

license.mjs
import { createPublicKey, verify } from 'node:crypto';
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';

const API = 'https://addonsmarket.com/api/licence';
const PUBLIQUE = '/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw=';
const SURSIS = 2592000;

const publique = createPublicKey({
  key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'),
                      Buffer.from(PUBLIQUE, 'base64')]),
  format: 'der',
  type: 'spki',
});

const canonique = (d) =>
  JSON.stringify(Object.fromEntries(Object.keys(d).sort().map((k) => [k, d[k]])));

const signee = (rep, hote) =>
  rep && rep.donnees && rep.signature
  && rep.donnees.hote === hote
  && verify(null, Buffer.from(canonique(rep.donnees), 'utf8'), publique,
            Buffer.from(rep.signature, 'base64'));

export async function etat(cle, hote, fichierCache) {
  const maintenant = Math.floor(Date.now() / 1000);
  let cache = null;

  try {
    const lu = JSON.parse(await readFile(fichierCache, 'utf8'));
    if (signee(lu, hote)) cache = lu;
  } catch {}

  if (cache && cache.donnees.revalider > maintenant) return cache.donnees;

  try {
    const r = await fetch(API, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'User-Agent': 'MonProduit/1.0' },
      body: JSON.stringify({ cle, url: hote }),
      signal: AbortSignal.timeout(6000),
    });
    if (!r.ok) throw new Error(String(r.status));

    const rep = await r.json();
    if (!signee(rep, hote)) throw new Error('signature');

    await mkdir(dirname(fichierCache), { recursive: true });
    await writeFile(fichierCache, JSON.stringify(rep));
    return rep.donnees;
  } catch {
    if (cache && cache.donnees.emis + SURSIS > maintenant) return cache.donnees;
    return { ok: false, motif: 'reseau' };
  }
}

Node 18 oder neuer: fetch wurde in 18 global, AbortSignal.timeout in 17.3. Ersetzen Sie die beiden auf einer älteren Laufzeit durch https.request und einen eigenen Timer.

Rufen Sie ihn dann einmal pro Anfrage auf, und blockieren Sie nie bei einem Netzwerkfehler.

server.mjs
import { etat } from './licence.mjs';

const d = await etat(process.env.LICENCE, req.hostname, './var/licence.json');

if (!d.ok && d.motif !== 'reseau') {
  return res.status(403).json({ erreur: d.motif });
}
UI React, Vue, Angular API

Es gibt keinen ehrlichen Weg, eine Lizenz im Browser zu prüfen. Was Sie schreiben, läuft auf dem Rechner des Käufers, in Code, den er lesen kann, und es zu entfernen kostet eine Zeile in den Entwicklerwerkzeugen. Wir liefern keine Frontend-Bibliothek, weil wir Ihnen damit ein Placebo verkaufen würden.

Hat Ihr Produkt ein Backend, prüfen Sie dort und geben Sie das Urteil als Datum aus. Die Oberfläche kann dann etwas Nützliches sagen, ohne dass die Prüfung selbst im Browser liegt.

server.mjs
app.get('/api/etat', async (req, res) => {
  const d = await etat(process.env.LICENCE, req.hostname, './var/licence.json');
  res.json({ actif: d.ok, essai: d.essai });
});
License.jsx
const [actif, setActif] = useState(null);

useEffect(() => {
  fetch('/api/etat')
    .then((r) => r.json())
    .then((d) => setActif(d.actif));
}, []);

if (actif === false) {
  return <Bandeau>Licence inactive</Bandeau>;
}

Ist Ihr Produkt eine reine Frontend-Vorlage ganz ohne Backend, gibt es zur Laufzeit nichts zu prüfen, und Sie sollten nicht so tun als ob. Die Lizenz greift dort, wo sie kann: beim Herunterladen und über die Bedingungen, die Ihr Käufer angenommen hat.

SH Shopify API

Eine Shopify-App hat ein eigenes Backend: Nutzen Sie dort den Node- oder PHP-Client und senden Sie die Shop-Domain — die myshopify.com-Adresse oder die eigene Domain, wenn Sie darauf abstellen. Ein Shopify-Theme hat kein Backend, dafür gilt der Frontend-Abschnitt oben.

PY Python API Python 3.7+

Standardbibliothek plus cryptography für die Signatur. Dieselben Regeln wie überall: 24 Stunden wiederverwenden, das letzte Urteil 30 Tage behalten, wenn wir schweigen, und nur bei einer signierten Ablehnung blockieren.

license.py
import base64, json, time, urllib.request
from pathlib import Path
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

API = 'https://addonsmarket.com/api/licence'
PUBLIQUE = '/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw='
SURSIS = 2592000

_publique = Ed25519PublicKey.from_public_bytes(base64.b64decode(PUBLIQUE))


def _canonique(d):
    return json.dumps(d, sort_keys=True, separators=(',', ':'), ensure_ascii=False)


def _signee(rep, hote):
    try:
        if rep['donnees']['hote'] != hote:
            return False
        _publique.verify(base64.b64decode(rep['signature']),
                         _canonique(rep['donnees']).encode('utf-8'))
        return True
    except (KeyError, TypeError, ValueError, InvalidSignature):
        return False


def etat(cle, hote, fichier_cache):
    maintenant = int(time.time())
    chemin = Path(fichier_cache)
    cache = None

    try:
        lu = json.loads(chemin.read_text(encoding='utf-8'))
        if _signee(lu, hote):
            cache = lu
    except (OSError, ValueError):
        pass

    if cache and cache['donnees']['revalider'] > maintenant:
        return cache['donnees']

    try:
        corps = json.dumps({'cle': cle, 'url': hote}).encode('utf-8')
        requete = urllib.request.Request(API, data=corps, headers={
            'Content-Type': 'application/json',
            'User-Agent': 'MonProduit/1.0',
        })
        with urllib.request.urlopen(requete, timeout=6) as r:
            rep = json.loads(r.read().decode('utf-8'))

        if not _signee(rep, hote):
            raise ValueError('signature')

        chemin.parent.mkdir(parents=True, exist_ok=True)
        chemin.write_text(json.dumps(rep), encoding='utf-8')
        return rep['donnees']

    except Exception:
        if cache and cache['donnees']['emis'] + SURSIS > maintenant:
            return cache['donnees']
        return {'ok': False, 'motif': 'reseau'}
{ } Rohes HTTP API

Für alles andere — Go, Ruby, .NET, Java, ein Shell-Skript. Ein POST, ein JSON-Rumpf. Die Felder stehen im API-Abschnitt weiter unten, und was Sie prüfen müssen, bevor Sie der Antwort glauben, im Signatur-Abschnitt.

curl
curl -X POST https://addonsmarket.com/api/licence \
     -H 'Content-Type: application/json' \
     -H 'User-Agent: MonProduit/1.0' \
     -d '{"cle":"VOTRE_CLE","url":"boutique.exemple.fr"}'

Etwas unklar?

Schreiben Sie uns aus Ihrem Autorenbereich. Eine Frage, die gestellt werden musste, heißt meistens, dass dieser Seite ein Absatz fehlt.

Autorenbereich