Integración del SDK
Integración del SDK de Java
Añade licenciamiento de Keyright a una aplicación Java: añade la dependencia com.delta1labs:keyright, verifica licencias sin conexión, activa en línea, restringe características según los derechos, y gestiona pruebas y máquinas aisladas (air-gapped) — fallando cerrado por diseño.
Esta página es la versión de extremo a extremo del cliente de Java: desde añadir la dependencia hasta una aplicación JVM lista para distribuir que desbloquea características de pago contra una clave de licencia real. El artefacto com.delta1labs:keyright verifica los mismos formatos de licencia y lease que los SDK de .NET, Node.js y Python, de modo que una línea de productos multilenguaje puede compartir un tenant de Keyright y una clave pública.
La configuración del proveedor — crear un producto, obtener tu clave pública, definir niveles y derechos, emitir claves — es independiente del lenguaje y se cubre una vez en Primeros pasos y, en profundidad para desarrolladores, en la guía Integración del SDK de .NET. Esta página asume que ya lo has hecho y se centra en el código de cliente de Java.
El modelo en un párrafo
Emites claves de licencia del lado del servidor en Keyright; cada tenant las firma con una clave privada RSA que nunca sale del servidor. Tu aplicación embebe solo la clave pública correspondiente y verifica licencias sin conexión contra ella, de modo que una comprobación de licencia no necesita red. Para la aplicación de puestos y la revocación la aplicación también activa en línea: intercambia una clave de licencia por un lease firmado de vida corta vinculado a la máquina, y luego sigue funcionando sin conexión hasta que ese lease caduca. Cualquier cosa que no cuadre — firma errónea, producto equivocado, caducidad, revocación, un reloj retrasado — se resuelve a la edición Free en lugar de lanzar una excepción. Falla cerrado.
Paso 1 — Añade la dependencia
El SDK se publica en com.delta1labs:keyright. Tiene como objetivo Java 17+, vive en el paquete com.delta1labs.keyright y no tiene dependencias en tiempo de ejecución (usa solo el cliente java.net.http del propio JDK y crypto).
Maven:
<dependency>
<groupId>com.delta1labs</groupId>
<artifactId>keyright</artifactId>
<version>1.1.4</version>
</dependency>
Gradle (Kotlin DSL):
implementation("com.delta1labs:keyright:1.1.4")
Paso 2 — Inicializa el cliente
Construye un KeyrightClient en el arranque con la fábrica estática initialize. KeyrightOptions expone setters fluidos para los campos comunes; pasa el slug del producto para el que emites claves, la clave pública en base64 de la pestaña Integration de tu panel y — para la activación en línea — la URL de tu servicio:
import com.delta1labs.keyright.KeyrightClient;
import com.delta1labs.keyright.KeyrightOptions;
public final class Licensing {
public static final KeyrightClient CLIENT = KeyrightClient.initialize(
new KeyrightOptions()
.product("acme-app") // must match the product slug you issue keys for
.publicKeyBase64("MIIBIjANBgkq...") // base64 SubjectPublicKeyInfo from the Integration tab
.serviceUrl("https://keyright.delta1labs.com")); // omit if you ship offline license files only
}
Solo publicKeyBase64 es estrictamente obligatorio — initialize lanza IllegalArgumentException si falta. La clave pública no es un secreto: solo puede verificar firmas, nunca acuñarlas, así que distribuirla dentro de tu JAR es seguro.
Los setters fluidos cubren los campos que normalmente tocas: product, publicKeyBase64, serviceUrl, licenseString, licenseFilePath, envVarName (por defecto "KEYRIGHT_LICENSE"), configLicensePath, defaultLicensePath, leaseCachePath, revocationListJson, localStatePath, now, machineComponents, clockTamperToleranceHours y transport. Unas pocas opciones son campos públicos simples sin setter fluido — defínelos directamente en el objeto de opciones:
KeyrightOptions opts = new KeyrightOptions()
.product("acme-app")
.publicKeyBase64("MIIBIjANBgkq...")
.serviceUrl("https://keyright.delta1labs.com");
// keep an old key valid during a rotation window (public field, no setter)
opts.additionalPublicKeysBase64.add("<previous public key>");
// loosen node-lock tolerance if you fingerprint volatile hardware (default 1)
opts.nodeLockTolerance = 2;
// ship a signed revocation list read from a file (public field, no setter)
opts.revocationListPath = "/opt/acme/revocations.json";
KeyrightClient client = KeyrightClient.initialize(opts);
El SDK resuelve una licencia a partir de varias fuentes, de mayor a menor precedencia: un licenseString explícito, luego un licenseFilePath explícito, luego el archivo nombrado por la variable de entorno envVarName, luego configLicensePath, luego el valor por defecto de datos de aplicación del SO (<LocalAppData>/Keyright/<product>/license.json), y finalmente el lease de activación en caché (.../lease.json). Normalmente no defines ninguna de estas rutas — la activación (Paso 4) escribe la caché del lease por ti.
Paso 3 — Restringe características sin conexión
Llama a validate(). Resuelve la mejor licencia de las fuentes de arriba, verifica la firma RSA, la coincidencia de producto, el bloqueo por equipo, la caducidad, la lista de revocación opcional distribuida y el estado de prueba/manipulación-de-reloj — todo sin conexión, y nunca lanza una excepción. Devuelve un KeyrightClient.Result con dos campos públicos: info (un LicenseInfo) y source (qué fuente ganó, un KeyrightClient.Source):
import com.delta1labs.keyright.KeyrightClient;
import com.delta1labs.keyright.Edition;
import com.delta1labs.keyright.LicenseInfo;
KeyrightClient.Result result = client.validate();
LicenseInfo info = result.info;
if (info.isPaid()) { // true for any edition above Free
// unlock paid features
}
if (info.edition == Edition.ENTERPRISE) {
// unlock enterprise-only features
}
// diagnostics: which source resolved the license?
if (result.source == KeyrightClient.Source.LEASE) {
// running on a cached activation lease
}
Cuando solo necesitas la licencia y no la fuente, validateInfo() devuelve el LicenseInfo directamente.
Ten en cuenta que en LicenseInfo los datos de la licencia se exponen como campos públicos finales, mientras que cualquier cosa calculada es un método. Los campos son status (un LicenseStatus), edition (un Edition), licensee, expiryUtc (un Instant, anulable), isTrial (un boolean), trialDays (un Integer, anulable), product, licenseId, entitlements (un EntitlementSet) y message. Los helpers derivados son métodos: isValid() (el estado es VALID), isPaid() (la edición está por encima de FREE), daysRemaining() (un Integer, o null para una licencia perpetua), isExpiringSoon(int withinDays), kind() (TRIAL / SUBSCRIPTION / PERPETUAL) y statusBadge() (p. ej. "Enterprise" o "Enterprise Trial").
Restringe según los derechos, no solo la edición
Restringe características individuales según los derechos para que cambiar la plantilla de un nivel no implique distribuir código nuevo. EntitlementSet tiene dos accesores — isEnabled(name) para banderas y getLimit(name, fallback) para límites numéricos — y ambos fallan cerrado:
import com.delta1labs.keyright.EntitlementSet;
import com.delta1labs.keyright.LicenseInfo;
LicenseInfo info = client.validateInfo();
EntitlementSet ent = info.entitlements;
// Boolean flag — a missing or unrecognized value reads as disabled
if (ent.isEnabled("export")) {
showExportCommand();
}
// Numeric limit — pass the fail-closed fallback yourself.
// "unlimited" resolves to EntitlementSet.MAXLONG (Long.MAX_VALUE).
long maxProjects = ent.getLimit("max-projects", 1);
if (currentProjectCount >= maxProjects) {
promptToUpgrade();
}
isEnabled trata "true", "1", "yes" y "enabled" (sin distinguir mayúsculas/minúsculas) como activado; cualquier otra cosa, incluido un nombre ausente, está desactivado. getLimit analiza el valor como un long, mapea el literal "unlimited" a EntitlementSet.MAXLONG y devuelve tu fallback cuando el nombre está ausente o no es analizable. Combina una comprobación de derecho con la edición cuando una característica está a la vez restringida y es específica de un nivel — y lee un LicenseInfo validado una vez, luego reutilízalo, en lugar de revalidar por cada comprobación:
LicenseInfo info = client.validateInfo();
boolean advancedExport =
info.edition == Edition.ENTERPRISE
&& info.entitlements.isEnabled("advanced-export");
El cliente también expone métodos de conveniencia de una línea — client.isEnabled("export"), client.getLimit("max-projects", 1), client.entitlements() y client.edition() — pero cada uno de ellos valida en el acto, así que prefiere un solo validateInfo() cuando compruebes varias cosas juntas.
Para averiguar por qué falló una comprobación (para un diálogo de “Register” o diagnóstico), lee info.status y el info.message orientado a personas.
Paso 4 — Activa en línea con una clave de licencia
Cuando un cliente introduce una clave, llama a activate(key). Es síncrono y hace POST de la clave más un id de máquina estable a tu servicio, que consume un puesto y devuelve un lease firmado de vida corta vinculado a esa máquina. El SDK verifica el lease contra tu clave pública embebida y lo almacena en caché localmente, de modo que cada validate() posterior tiene éxito sin red hasta que transcurre la ventana de gracia del lease.
activate no lanza excepción para las rutas de fallo ordinarias (clave errónea, límite de puestos, sin conexión, revocada) — como validate(), devuelve un LicenseInfo que falla cerrado. Inspecciona el resultado:
LicenseInfo info = client.activate(customerEnteredKey);
if (info.isValid() && info.isPaid()) {
// Activated. The lease is cached; the app now works offline until it expires.
showLicensedUi(info.statusBadge()); // e.g. "Enterprise" or "Enterprise Trial"
} else {
// Surface info.message; the app stays in Free mode.
showActivationError(info.message); // "All seats for this license are in use.", etc.
}
- Los puestos se aplican del lado del servidor. Activar más máquinas de las que la licencia permite devuelve un resultado de límite de puestos (estado
NO_LICENSE, mensaje “All seats for this license are in use.”) y ningún lease. Reactivar una máquina que ya está vinculada es idempotente — no se consume un puesto extra. - Gracia sin conexión. Si el servicio es inalcanzable,
activaterecurre a cualquier lease en caché que siga siendo válido, de modo que una breve caída no deja fuera al usuario. Solo cuando no hay un lease en caché válido devuelve un resultado que falla cerrado “Could not reach issuing service”. - La revocación surte efecto en el siguiente refresco — el cliente cae a
Freecon estadoREVOKED.
activate solo lanza excepción por errores de programación: IllegalStateException si no se configuró serviceUrl, o IllegalArgumentException por una clave vacía.
El id de máquina que envía es el mismo que puedes mostrar para soporte:
import com.delta1labs.keyright.MachineFingerprint;
String machineId = MachineFingerprint.current().toBoundString(); // e.g. "KRM1:...:...:..."
Paso 5 — Desactiva (libera un puesto)
Para mover una licencia a otra máquina, libera primero el puesto de esta máquina. deactivate(key) hace POST a tu servicio y devuelve la cadena de estado del servicio ("ok" o "not_found"). Ante "ok" borra el lease en caché local, de modo que las comprobaciones sin conexión posteriores recaen correctamente en Free.
A diferencia de la activación, la desactivación no tiene fallback sin conexión — liberar un puesto es un cambio del lado del servidor, así que si el servicio no puede alcanzarse (o responde no-2xx o de forma ilegible) lanza IllegalStateException:
try {
String status = client.deactivate(customerEnteredKey);
if ("ok".equals(status)) {
showMessage("This machine's seat has been released.");
} else {
showMessage("No active seat was found for this key on this machine.");
}
} catch (IllegalStateException ex) {
// service unreachable or returned an error — the seat was NOT released
showError("Couldn't reach the licensing service. Try again while online.");
}
Paso 6 — Máquinas aisladas (activación sin conexión)
Para una máquina que nunca puede alcanzar el servicio, un operador firma un lease sin conexión para su id de máquina (panel Licenses → offline lease, o POST /admin/licenses/{id}/offline-lease) y entrega el JSON por archivo. Obtén el id de la máquina destino con MachineFingerprint.current().toBoundString(), entrégalo al operador, luego importa el lease devuelto.
importOfflineLease(json) verifica el lease contra tu(s) clave(s) pública(s), producto, máquina y caducidad — la misma ruta que usa validate() — y lo almacena en caché localmente para que las comprobaciones posteriores tengan éxito sin red. Lanza excepción si el lease es inválido: IllegalArgumentException por entrada ausente o que no es JSON, IllegalStateException si la firma, el producto, la máquina o la caducidad no cuadran.
import java.nio.file.Files;
import java.nio.file.Path;
try {
String leaseJson = Files.readString(Path.of("acme.lease.json"));
LicenseInfo info = client.importOfflineLease(leaseJson);
showLicensedUi(info.statusBadge());
} catch (IllegalArgumentException | IllegalStateException ex) {
// wrong signature / product / machine, or expired
showError("That offline lease isn't valid for this machine: " + ex.getMessage());
}
Paso 7 — Bloqueo por equipo, pruebas y revocación
- Bloqueo por equipo. El SDK deriva una huella digital de máquina estable del GUID de máquina del SO, el nombre de host y el descriptor de SO, y es compatible a nivel de bytes con los demás SDK. Una pequeña
nodeLockTolerance(por defecto1) significa que una NIC o un disco cambiados no dejan fuera al usuario. La petición de activación envíaMachineFingerprint.current().toBoundString()para que los puestos se cuenten por dispositivo;MachineFingerprint.current().displayId()da un id corto y legible por personasKR-…para soporte. Para controlar qué identifica a una máquina (por ejemplo en un contenedor o en una prueba), definemachineComponentsen las opciones como una lista de pares de cadenas{key, value}. - Pruebas. Una clave de prueba se activa por la misma ruta
activateexacta que una de pago. Muestra el estado directamente desdeLicenseInfo—info.isTrial,info.statusBadge()(p. ej. “Enterprise Trial”),info.expiryUtceinfo.daysRemaining()para una insignia de “27 days left”. Una prueba con un recuentotrialDaysy sin caducidad fija se ancla a la primera activación en esta máquina y se cuenta atrás localmente; se reemplaza sin fisuras cuando el cliente activa luego una clave de pago. Flujo completo: Pruebas gratuitas de autoservicio. - Revocación. Revoca una clave desde el panel (o
POST /admin/licenses/{id}/revoke); el cliente cae aFree(estadoREVOKED) en el siguiente refresco del lease. Para una aplicación puramente sin conexión, distribuye una lista de revocación firmada con tu compilación medianterevocationListJson(setter fluido) orevocationListPath(campo público) para que siga honrando las revocaciones. - Rotación de claves. Cuando rotas la clave de firma del tenant, distribuye una compilación con la nueva clave pública en
publicKeyBase64y la saliente añadida aadditionalPublicKeysBase64— las licencias y leases firmados por cualquiera de las dos siguen validándose durante la transición. Quita la clave antigua una vez que todo lease firmado por ella haya caducado.
Solución de problemas
- La activación dice que la clave no se reconoció. La causa más común es una discrepancia de producto: la clave se emitió para un producto (o tenant) distinto del slug
productcon el que tu cliente está configurado. Confirma que el slug del Paso 2 coincide con el producto bajo el que emitiste la clave. - Todo se lee como Free. Es el diseño — el SDK falla cerrado ante cualquier problema de verificación en lugar de lanzar una excepción. Lee
info.status(NO_LICENSE,MALFORMED,SIGNATURE_INVALID,EXPIRED,MACHINE_MISMATCH,REVOKED,CLOCK_TAMPERED) einfo.messagepara ver cuál es. UnSIGNATURE_INVALIDcasi siempre significa que la clave pública embebida no coincide con el tenant que firmó la clave. - Una licencia por tiempo limitado de repente no valida. Si el reloj del sistema se mueve hacia atrás más allá de
clockTamperToleranceHours(por defecto 24h) en una prueba o suscripción, el SDK devuelve el estadoCLOCK_TAMPERED. No se queda pegado — ajusta la hora correcta y la validación se recupera en la siguiente llamada. Las licencias perpetuas nunca están sujetas a esta comprobación. IllegalStateExceptiondeactivateodeactivate. Estas lanzan excepción solo por problemas de configuración/conectividad: ningúnserviceUrldefinido, o (paradeactivate) que el servicio sea inalcanzable. Los resultados ordinarios de licenciamiento nunca lanzan excepción — inspecciona en su lugar elLicenseInfo/cadena de estado devuelto.
Véase también
- Primeros pasos — la ruta condensada del proveedor y la lista de verificación para producción.
- Integración del SDK de .NET — el mismo viaje con la configuración del proveedor en plena profundidad para desarrolladores.
- Activación y ciclo de vida del lease — cómo encajan la activación, los leases, la gracia sin conexión y el refresco.
- Seguridad y leases — el modelo de firma, qué es un secreto y qué no, y la verificación del lease.
- Referencia de la API HTTP — el mapa completo de endpoints detrás de estas llamadas.
- Repositorio de samples — ejemplos de cliente ejecutables.