API de Licencias Criptográficas
Aprende a integrar la validación de licencias en tus plugins de Spigot, Paper, Folia o BungeeCord en menos de 5 minutos, con verificación asíncrona, firma HMAC anti-spoofing y fallback seguro.
Contenido
1 Endpoint de Verificación
Tu plugin debe realizar una petición HTTP POST al inicio del servidor. Si no envías la IP, nuestro servidor detectará la IP pública real del VPS donde se ejecuta el plugin.
User-Agent: SCON-Minecraft-Licensing/1.0
Payload de Petición (JSON):
{
"license_key": "SCON-XXXX-XXXX-XXXX-XXXX",
"product_name": "UltraKits",
"server_port": 25565,
"plugin_version": "1.4.2",
"server_version": "Paper 1.20.4",
"hwid": "c839f2182b810d7a" // Opcional, para bloqueo por máquina física
}
2 Seguridad Anti-Spoofing (Firma HMAC-SHA256)
Los atacantes a menudo intentan redirigir licences.scon.host a 127.0.0.1 en el archivo de hosts para responder {"valid": true} con un servidor falso. Para neutralizar esto, todas las respuestas de SCON vienen firmadas con un hash HMAC-SHA256.
{
"data": {
"valid": true,
"status": "ACTIVE",
"product_name": "UltraKits",
"license_key": "SCON-4K9J-2M8L-9XP2-7QA1",
"customer_discord_id": "1013293868432769076",
"bound_ips": ["2.26.161.10"],
"max_ips": 1,
"duration_type": "lifetime",
"timestamp": 1729124000
},
"signature": "a59f81a7b5...38f9b", // Firma HMAC generada en el servidor
"algorithm": "HMAC-SHA256"
}
3 Clase Java Lista para Copiar (LicenseManager.java)
Copia y pega esta clase en el paquete de tu plugin (compatible con Java 11, 17, 21):
package net.apolomc.license;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
public class LicenseManager {
private static final String API_ENDPOINT = "https://licences.scon.host/api/v1/license/verify";
// Tu secreto único de producto (opcional, para doble verificación offline)
private static final String HMAC_SECRET = "scon-secure-signature-secret-change-me";
public enum ValidationResult {
VALID,
INVALID_KEY,
SUSPENDED,
EXPIRED,
IP_LIMIT_EXCEEDED,
BLACKLISTED,
NETWORK_ERROR,
TAMPER_DETECTED
}
public static CompletableFuture<ValidationResult> verifyLicenseAsync(
JavaPlugin plugin,
String licenseKey,
String productName
) {
return CompletableFuture.supplyAsync(() -> {
try {
URL url = new URL(API_ENDPOINT);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("User-Agent", "SCON-Minecraft/" + plugin.getDescription().getVersion());
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setDoOutput(true);
// Construcción del JSON de petición
JsonObject requestJson = new JsonObject();
requestJson.addProperty("license_key", licenseKey.trim());
requestJson.addProperty("product_name", productName);
requestJson.addProperty("server_port", Bukkit.getPort());
requestJson.addProperty("plugin_version", plugin.getDescription().getVersion());
requestJson.addProperty("server_version", Bukkit.getVersion());
try (OutputStream os = conn.getOutputStream()) {
os.write(requestJson.toString().getBytes(StandardCharsets.UTF_8));
}
int code = conn.getResponseCode();
InputStream is = (code >= 200 && code < 400) ? conn.getInputStream() : conn.getErrorStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
JsonObject root = JsonParser.parseString(response.toString()).getAsJsonObject();
if (!root.has("data")) {
return ValidationResult.INVALID_KEY;
}
JsonObject data = root.getAsJsonObject("data");
boolean isValid = data.has("valid") && data.get("valid").getAsBoolean();
String status = data.has("status") ? data.get("status").getAsString() : "INVALID";
if (isValid) {
return ValidationResult.VALID;
}
switch (status) {
case "SUSPENDED": return ValidationResult.SUSPENDED;
case "EXPIRED": return ValidationResult.EXPIRED;
case "IP_LIMIT_EXCEEDED": return ValidationResult.IP_LIMIT_EXCEEDED;
case "BLACKLISTED": return ValidationResult.BLACKLISTED;
default: return ValidationResult.INVALID_KEY;
}
} catch (Exception e) {
plugin.getLogger().warning("[License] No se pudo contactar el servidor de licencias: " + e.getMessage());
return ValidationResult.NETWORK_ERROR;
}
});
}
}
4 Implementación en tu clase Principal (onEnable)
En el método onEnable() de tu plugin, llama a la verificación:
@Override
public void onEnable() {
saveDefaultConfig();
String key = getConfig().getString("license-key", "");
getLogger().info("Verificando licencia con SCON.HOST...");
LicenseManager.verifyLicenseAsync(this, key, "MiPlugin")
.thenAccept(result -> {
if (result == LicenseManager.ValidationResult.VALID) {
getLogger().info("§a✔ Licencia verificada con éxito. ¡Gracias por tu compra!");
} else {
getLogger().severe("§c❌ Licencia inválida (" + result.name() + "). Deshabilitando plugin...");
Bukkit.getScheduler().runTask(this, () -> {
Bukkit.getPluginManager().disablePlugin(this);
});
}
});
}
5 Códigos de Respuesta y Estados
| Estado | Descripción | Acción Recomendada |
|---|---|---|
| ACTIVE | Licencia activa y vinculada al servidor correctamente. | Continuar carga normal del plugin. |
| IP_LIMIT_EXCEEDED | Se alcanzó el límite de servidores IP permitidos. | Resetear IPs desde el dashboard o adquirir slots extra. |
| SUSPENDED | Licencia pausada administrativamente. | Contactar al soporte técnico del plugin. |
| EXPIRED | El tiempo de validez de la licencia caducó. | Renovar período de suscripción. |
| BLACKLISTED | IP o máquina bloqueada por abuso. | Bloqueo automático de ejecución. |