杰瑞科技汇

java license 实现

  1. 许可证颁发端:由软件提供商(你)使用,用于生成和签发许可证文件。
  2. 许可证验证端:集成到你的应用程序中,用于在运行时验证许可证的有效性。

我们将围绕这两个部分,使用 Java 和一些流行的库来构建一个安全、可靠的实现方案。

java license 实现-图1
(图片来源网络,侵删)

核心技术选型

为了实现一个安全的许可证系统,我们不能简单地将许可证信息(如有效期、用户名)存为明文文本,必须使用非对称加密来确保许可证的真实性完整性

  • 非对称加密:使用一对密钥:公钥和私钥。
    • 私钥:由软件提供商保密,用于对许可证信息进行签名
    • 公钥:可以公开,集成到你的应用程序中,用于验证签名。
  • 数字签名:通过对许可证信息的哈希值进行加密,确保信息在传输过程中没有被篡改,并且确实是由你签发的。

我们将使用以下库:

  • Bouncy Castle:一个功能强大的加密库,广泛用于 Java 应用中,我们将用它来处理密钥对和签名。
  • Jackson:一个流行的 JSON 库,用于将许可证信息对象序列化为 JSON 字符串,便于存储和传输。

第一步:项目设置

创建一个 Maven 项目,并添加以下依赖到你的 pom.xml 文件中:

<dependencies>
    <!-- 用于处理 JSON 数据 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>
    <!-- 强大的加密库 -->
    <dependency>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcprov-jdk15on</artifactId>
        <version>1.70</version>
    </dependency>
</dependencies>

第二步:许可证数据模型

我们定义一个 Java 类来表示许可证包含的所有信息,这个类将被序列化为 JSON。

java license 实现-图2
(图片来源网络,侵删)

LicenseInfo.java

import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Instant;
import java.util.Date;
public class LicenseInfo {
    private String productName;
    private String licensee;
    private Date expiryDate;
    private int maxUsers;
    private String licenseType; // e.g., "STANDARD", "PRO", "ENTERPRISE"
    // Jackson 需要一个无参构造函数
    public LicenseInfo() {}
    public LicenseInfo(String productName, String licensee, Date expiryDate, int maxUsers, String licenseType) {
        this.productName = productName;
        this.licensee = licensee;
        this.expiryDate = expiryDate;
        this.maxUsers = maxUsers;
        this.licenseType = licenseType;
    }
    // Getters and Setters
    public String getProductName() { return productName; }
    public void setProductName(String productName) { this.productName = productName; }
    public String getLicensee() { return licensee; }
    public void setLicensee(String licensee) { this.licensee = licensee; }
    @JsonProperty("expiryDate") // 使用 Jackson 注解确保序列化/反序列化时字段名一致
    public Date getExpiryDate() { return expiryDate; }
    public void setExpiryDate(Date expiryDate) { this.expiryDate = expiryDate; }
    public int getMaxUsers() { return maxUsers; }
    public void setMaxUsers(int maxUsers) { this.maxUsers = maxUsers; }
    public String getLicenseType() { return licenseType; }
    public void setLicenseType(String licenseType) { this.licenseType = licenseType; }
    @Override
    public String toString() {
        return "LicenseInfo{" +
                "productName='" + productName + '\'' +
                ", licensee='" + licensee + '\'' +
                ", expiryDate=" + expiryDate +
                ", maxUsers=" + maxUsers +
                ", licenseType='" + licenseType + '\'' +
                '}';
    }
}

第三步:密钥对管理

我们需要一个工具类来生成和加载 RSA 密钥对,在实际应用中,私钥必须被严格保密,最好存储在安全的地方,比如硬件安全模块 或一个加密的文件中。

KeyPairGeneratorUtil.java

import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.Date;
public class KeyPairGeneratorUtil {
    /**
     * 生成 RSA 密钥对
     * @param keySize 密钥大小,建议 2048 或更高
     * @return KeyPair
     * @throws NoSuchAlgorithmException
     */
    public static KeyPair generateKeyPair(int keySize) throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(keySize);
        return keyPairGenerator.generateKeyPair();
    }
    /**
     * 将密钥对保存到 PEM 文件
     * @param keyPair 密钥对
     * @param privateKeyPath 私钥文件路径
     * @param publicKeyPath 公钥文件路径
     * @throws IOException
     */
    public static void saveKeyPairToPem(KeyPair keyPair, String privateKeyPath, String publicKeyPath) throws IOException {
        saveKeyToPem(keyPair.getPrivate(), privateKeyPath);
        saveKeyToPem(keyPair.getPublic(), publicKeyPath);
    }
    private static void saveKeyToPem(Key key, String filePath) throws IOException {
        PemObject pemObject = new PemObject(key instanceof PrivateKey ? "PRIVATE KEY" : "PUBLIC KEY", key.getEncoded());
        try (PemWriter pemWriter = new PemWriter(new FileWriter(filePath))) {
            pemWriter.writeObject(pemObject);
        }
    }
    /**
     * 从 PEM 文件加载私钥
     * @param privateKeyPath 私钥文件路径
     * @return PrivateKey
     * @throws Exception
     */
    public static PrivateKey loadPrivateKeyFromPem(String privateKeyPath) throws Exception {
        // 实际实现中需要读取文件并解析 PEM 格式
        // 这里简化处理,假设你已经有了一个 PrivateKey 对象
        // 你可以使用 Bouncy Castle 的 PEMParser 来实现
        // 此处仅为示例,省略文件读取逻辑
        // return ... 从文件加载 ...
        throw new UnsupportedOperationException("请实现从文件加载私钥的逻辑");
    }
    /**
     * 从 PEM 文件加载公钥
     * @param publicKeyPath 公钥文件路径
     * @return PublicKey
     * @throws Exception
     */
    public static PublicKey loadPublicKeyFromPem(String publicKeyPath) throws Exception {
        // 同上,实际实现中需要读取文件并解析
        // throw new UnsupportedOperationException("请实现从文件加载公钥的逻辑");
        // 为了演示,我们直接返回一个公钥(实际应用中应从文件加载)
        // 注意:这只是一个占位符,你需要完整的实现
        return generateKeyPair(2048).getPublic();
    }
}

注意:上面的 loadPrivateKeyFromPemloadPublicKeyFromPem 是简化版,完整的实现需要使用 org.bouncycastle.openssl.PEMParser 来解析 PEM 文件,这里为了代码简洁,省略了文件 I/O 的细节,但你必须在实际项目中实现它。

java license 实现-图3
(图片来源网络,侵删)

第四步:许可证颁发端(签名)

这是软件提供商使用的部分,它负责创建 LicenseInfo 对象,将其序列化为 JSON,然后使用私钥对其进行签名,最后将签名和 JSON 数据组合成最终的许可证文件。

LicenseSigner.java

import com.fasterxml.jackson.databind.ObjectMapper;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class LicenseSigner {
    private static final ObjectMapper objectMapper = new ObjectMapper();
    /**
     * 签名许可证信息并生成许可证文件
     * @param licenseInfo 许可证数据对象
     * @param privateKey 私钥
     * @param licenseFilePath 输出的许可证文件路径
     * @throws Exception
     */
    public static void signAndSaveLicense(LicenseInfo licenseInfo, PrivateKey privateKey, String licenseFilePath) throws Exception {
        // 1. 将 LicenseInfo 对象序列化为 JSON 字符串
        String jsonData = objectMapper.writeValueAsString(licenseInfo);
        // 2. 使用 SHA256withRSA 算法创建签名器
        ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(privateKey);
        // 3. 对 JSON 数据进行签名
        byte[] signature = signer.generateSignature(jsonData.getBytes());
        // 4. 将 JSON 数据和 Base64 编码的签名组合
        String licenseContent = jsonData + "\n-----LICENSE SIGNATURE-----\n" + Base64.getEncoder().encodeToString(signature);
        // 5. 保存到文件
        Files.write(Paths.get(licenseFilePath), licenseContent.getBytes());
        System.out.println("许可证文件已成功生成并保存到: " + licenseFilePath);
    }
    public static void main(String[] args) {
        try {
            // 1. 准备数据
            LicenseInfo licenseInfo = new LicenseInfo(
                    "MyAwesomeSoftware",
                    "John Doe Corp",
                    Date.from(Instant.now().plusSeconds(365 * 24 * 60 * 60)), // 1年后过期
                    50,
                    "PRO"
            );
            // 2. 加载私钥 (在实际应用中,你应该从一个安全的地方加载)
            // 这里为了演示,我们生成一个临时的。
            // 注意:真实项目中,私钥应存储在安全位置,而不是每次都生成!
            KeyPair keyPair = KeyPairGeneratorUtil.generateKeyPair(2048);
            PrivateKey privateKey = keyPair.getPrivate();
            // 保存私钥到文件(仅作演示,实际应用中请妥善保管!)
            KeyPairGeneratorUtil.saveKeyPairToPem(keyPair, "private_key.pem", "public_key.pem");
            System.out.println("密钥对已生成并保存到 private_key.pem 和 public_key.pem");
            // 3. 签名并保存许可证
            signAndSaveLicense(licenseInfo, privateKey, "license.lic");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行 LicenseSignermain 方法后,你会在项目根目录下得到三个文件:

  • private_key.pem必须保密!
  • public_key.pem:可以公开,集成到你的客户端应用中。
  • license.lic:最终发放给客户的许可证文件,内容类似这样:
    {"productName":"MyAwesomeSoftware","licensee":"John Doe Corp","expiryDate":"2025-10-27T10:30:00.000+00:00","maxUsers":50,"licenseType":"PRO"}
    -----LICENSE SIGNATURE-----
    [一长串Base64编码的签名]

第五步:许可证验证端(集成到应用中)

这是你的应用程序在启动或需要验证许可证时调用的部分,它读取 license.lic 文件,分离出 JSON 数据和签名,然后使用公钥验证签名是否有效。

LicenseValidator.java

import com.fasterxml.jackson.databind.ObjectMapper;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.PublicKey;
import java.security.Signature;
import java.util.Base64;
public class LicenseValidator {
    private static final ObjectMapper objectMapper = new ObjectMapper();
    /**
     * 验证许可证文件
     * @param licenseFilePath 许可证文件路径
     * @param publicKey 公钥
     * @return 验证结果对象
     * @throws Exception
     */
    public static ValidationResult validateLicense(String licenseFilePath, PublicKey publicKey) throws Exception {
        // 1. 读取许可证文件内容
        String licenseContent = new String(Files.readAllBytes(Paths.get(licenseFilePath)));
        // 2. 分离 JSON 数据和签名
        String[] parts = licenseContent.split("\n-----LICENSE SIGNATURE-----\n");
        if (parts.length != 2) {
            return new ValidationResult(false, "许可证文件格式无效。");
        }
        String jsonData = parts[0];
        String signatureBase64 = parts[1];
        // 3. 将 Base64 签名解码为字节数组
        byte[] signature = Base64.getDecoder().decode(signatureBase64);
        // 4. 验证签名
        Signature verifier = Signature.getInstance("SHA256withRSA");
        verifier.initVerify(publicKey);
        verifier.update(jsonData.getBytes());
        boolean isSignatureValid = verifier.verify(signature);
        if (!isSignatureValid) {
            return new ValidationResult(false, "许可证签名无效,可能被篡改。");
        }
        // 5. 签名有效,现在解析 JSON 数据
        LicenseInfo licenseInfo = objectMapper.readValue(jsonData, LicenseInfo.class);
        // 6. 检查其他业务逻辑,例如有效期
        if (licenseInfo.getExpiryDate().before(new Date())) {
            return new ValidationResult(false, "许可证已过期。");
        }
        // 所有检查都通过
        return new ValidationResult(true, "许可证验证成功。", licenseInfo);
    }
    public static void main(String[] args) {
        try {
            // 1. 加载公钥 (在实际应用中,公钥可以硬编码在应用中,或从安全位置加载)
            // 这里我们从之前生成的文件中加载
            PublicKey publicKey = KeyPairGeneratorUtil.loadPublicKeyFromPem("public_key.pem");
            // 2. 验证许可证
            ValidationResult result = validateLicense("license.lic", publicKey);
            // 3. 输出验证结果
            System.out.println("验证状态: " + (result.isValid() ? "成功" : "失败"));
            System.out.println("验证信息: " + result.getMessage());
            if (result.isValid() && result.getLicenseInfo() != null) {
                System.out.println("许可证详情: " + result.getLicenseInfo());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/**
 * 验证结果封装类
 */
class ValidationResult {
    private final boolean isValid;
    private final String message;
    private final LicenseInfo licenseInfo;
    public ValidationResult(boolean isValid, String message) {
        this(isValid, message, null);
    }
    public ValidationResult(boolean isValid, String message, LicenseInfo licenseInfo) {
        this.isValid = isValid;
        this.message = message;
        this.licenseInfo = licenseInfo;
    }
    public boolean isValid() { return isValid; }
    public String getMessage() { return message; }
    public LicenseInfo getLicenseInfo() { return licenseInfo; }
}

运行 LicenseValidatormain 方法,license.lic 文件没有被篡改且未过期,你会看到:

验证状态: 成功
验证信息: 许可证验证成功。
许可证详情: LicenseInfo{productName='MyAwesomeSoftware', licensee='John Doe Corp', expiryDate=Wed Oct 27 10:30:00 CST 2025, maxUsers=50, licenseType='PRO'}

如果你修改 license.lic 文件中的 JSON 数据(比如把 John Doe Corp 改成 Hacker),再运行验证,你会得到:

验证状态: 失败
验证信息: 许可证签名无效,可能被篡改。

高级考虑与最佳实践

上面的实现是一个基础框架,对于生产环境,还需要考虑更多方面:

  1. 更复杂的许可证逻辑

    • 绑定硬件:将许可证与用户机器的硬件特征(如 MAC 地址、主板序列号)绑定,防止随意复制,你需要在 LicenseInfo 中添加一个 hardwareFingerprint 字段,并在颁发和验证时进行比对。
    • 功能开关:许可证可以控制哪些功能可用。licenseType 为 "STANDARD" 的用户无法使用高级功能。
    • 使用次数/时间限制:记录许可证的使用次数或总运行时间。
  2. 私钥安全

    • 绝对不要将私钥硬编码在代码中!
    • 最佳实践:将私钥存储在离线的、安全的机器上,仅在需要生成许可证时使用。
    • 次优选择:如果必须将私钥放在服务器上,请使用密钥管理服务,如 AWS KMS, HashiCorp Vault,或使用操作系统提供的密钥库进行加密存储。
  3. 防止逆向工程

    • 验证逻辑(LicenseValidator)是集成在客户端应用的,如果有人想破解,他们可能会尝试反编译你的代码,绕过验证。
    • 代码混淆:使用 ProGuard 或 Zelix KlassMaster 等工具对你的客户端代码进行混淆,增加逆向工程的难度。
    • 完整性校验:在验证许可证的同时,也可以对你的应用程序自身进行签名或校验,确保它没有被修改。
  4. 在线激活与验证

    • 对于更高级的安全需求,可以设计一个在线激活服务器。
    • 流程变为:客户端生成一个硬件指纹 -> 发送到你的激活服务器 -> 服务器验证后,结合私钥生成一个离线许可证或返回一个短期激活令牌 -> 客户端使用这个令牌。
    • 这样,即使客户端的验证逻辑被破解,没有你服务器的配合也无法生成有效的许可证。
  5. 使用专业库

    • 如果你不想自己从头实现,可以考虑使用成熟的 Java 开源库,如 FlexNet 的某些开源实现,或者商业的软件许可解决方案,它们通常提供了更完善的功能和更高的安全性。

通过以上步骤,你就构建了一个健壮且安全的 Java 软件许可证实现,你可以根据自己产品的具体需求,在此基础上进行扩展和优化。

分享:
扫描分享到社交APP
上一篇
下一篇