🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

商業化必備:SpringBoot 實現許可證控制

image

背景與需求

在軟體開發和商業化過程中,許可證控制是一個不可避免的技術需求。無論是企業級管理系統、桌面應用軟體,還是SaaS服務,都需要對軟體的使用範圍、功能權限和時間限制進行有效管控。

許可證系統的核心價值在於:
保護知識產權:防止軟體被非法複製和分發

商業模式支撐:支持按功能、按時間的差異化定價

用戶管理:精確控制授權用戶和使用範圍

合規要求:滿足企業對軟體資產管理的需求

本文將介紹一個基於Spring Boot + RSA2048非對稱加密的許可證控制系統實現方案,具備硬體綁定、功能權限控制等。

設計思路

技術設計

許可證系統採用非對稱加密的設計思路:廠商使用私鑰對許可證資訊進行數位簽名,客戶端使用對應的公鑰驗證簽名的真實性。這種架構的優勢在於:

1. 安全性高:私鑰由廠商嚴格保管,公鑰可以隨軟體分發,即使公鑰洩露也無法偽造許可證

2. 部署簡單:無需額外的許可證伺服器,支持離線驗證

3. 擴展性強:可以靈活添加各種驗證規則和權限控制

技術選型

後端技術棧

  • Spring Boot 3.x:提供完整的Web服務框架和依賴注入
  • Java Security API:利用JDK內建的RSA加密演算法實現
  • Jackson:處理JSON序列化和反序列化

前端技術棧

  • 原生JavaScript:無框架依賴,保持輕量級
  • TailwindCSS:快速構建現代化UI介面
  • RESTful API:標準化的前後端交互

加密演算法

  • RSA2048:足夠安全的非對稱加密強度
  • SHA256withRSA:數位簽名演算法
  • Base64:簽名結果編碼格式

核心功能實現

硬體指紋獲取

硬體綁定是許可證系統的重要安全特性,通過獲取主板序列號實現設備唯一性識別。

@Component
public class HardwareUtil {
    private static final Logger logger = LoggerFactory.getLogger(HardwareUtil.class);

    /**
     * 獲取主板序列號,支持Windows和Linux系統
     */
    public String getMotherboardSerial() {
        String os = System.getProperty("os.name").toLowerCase();

        try {
            if (os.contains("windows")) {
                return getWindowsMotherboardSerial();
            } else if (os.contains("linux")) {
                return getLinuxMotherboardSerial();
            } else {
                logger.warn("不支持的操作系統: {}", os);
                return "UNKNOWN";
            }
        } catch (Exception e) {
            logger.error("獲取主板序列號失敗", e);
            return "UNKNOWN";
        }
    }

    /**
     * Windows系統通過WMI命令獲取主板序列號
     */
    private String getWindowsMotherboardSerial() throws Exception {
        Process process = Runtime.getRuntime().exec("wmic baseboard get serialnumber");
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)
        );

        String line;
        while ((line = reader.readLine()) != null) {
            line = line.trim();
            if (!line.isEmpty() && !line.equals("SerialNumber")) {
                logger.debug("Windows主板序列號: {}", line);
                return line;
            }
        }

        reader.close();
        process.waitFor();
        return "UNKNOWN";
    }

    /**
     * Linux系統通過dmidecode命令獲取主板序列號
     */
    private String getLinuxMotherboardSerial() throws Exception {
        try {
            // 優先使用dmidecode命令
            Process process = Runtime.getRuntime().exec("sudo dmidecode -s baseboard-serial-number");
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)
            );

            String line = reader.readLine();
            reader.close();
            process.waitFor();

            if (line != null && !line.trim().isEmpty() && !line.contains("Not Specified")) {
                logger.debug("Linux主板序列號: {}", line.trim());
                return line.trim();
            }

            // 备选方案:读取系统文件
            return getLinuxMotherboardFromSys();
        } catch (Exception e) {
            logger.error("dmidecode命令執行失敗", e);
            return getLinuxMotherboardFromSys();
        }
    }

    /**
     * 從/sys/class/dmi/id/board_serial文件讀取主板序列號
     */
    private String getLinuxMotherboardFromSys() {
        try {
            Process process = Runtime.getRuntime().exec("cat /sys/class/dmi/id/board_serial");
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)
            );

            String line = reader.readLine();
            reader.close();
            process.waitFor();

            if (line != null && !line.trim().isEmpty()) {
                logger.debug("Linux主板序列號(從sys讀取): {}", line.trim());
                return line.trim();
            }

        } catch (Exception e) {
            logger.warn("從/sys文件讀取失敗", e);
        }

        return "UNKNOWN";
    }

    /**
     * 獲取系統資訊摘要,用於調試和展示
     */
    public String getSystemInfo() {
        return String.format("操作系統: %s %s, 架構: %s, 主板序列號: %s",
            System.getProperty("os.name"),
            System.getProperty("os.version"),
            System.getProperty("os.arch"),
            getMotherboardSerial()
        );
    }
}

這個實現的關鍵點

異常處理:獲取失敗時返回"UNKNOWN"而不是拋出異常,保證程序穩定性

多重備選:Linux下優先使用dmidecode,失敗時嘗試讀取sys文件

編碼處理:統一使用UTF-8編碼避免亂碼問題

日誌記錄:詳細記錄獲取過程,便於問題排查

RSA加密工具類

RSA加密是整個系統的安全基石,需要提供密鑰生成、簽名、驗簽等完整功能。

@Component
public class RSAUtil {
    private static final Logger logger = LoggerFactory.getLogger(RSAUtil.class);
    private static final String ALGORITHM = "RSA";
    private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
    private static final int KEY_SIZE = 2048;

    /**
     * 生成RSA密鑰對
     */
    public KeyPair generateKeyPair() throws Exception {
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);
        keyGen.initialize(KEY_SIZE);
        KeyPair keyPair = keyGen.generateKeyPair();
        logger.info("RSA密鑰對生成成功,密鑰長度: {} bits", KEY_SIZE);
        return keyPair;
    }

    /**
     * 使用私鑰對資料進行數位簽名
     */
    public String sign(String data, PrivateKey privateKey) throws Exception {
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
        signature.initSign(privateKey);
        signature.update(data.getBytes(StandardCharsets.UTF_8));
        byte[] signedBytes = signature.sign();
        String result = Base64.getEncoder().encodeToString(signedBytes);
        logger.debug("資料簽名完成,原始資料長度: {}, 簽名長度: {}", data.length(), result.length());
        return result;
    }

    /**
     * 使用公鑰驗證數位簽名
     */
    public boolean verify(String data, String signatureBase64, PublicKey publicKey) throws Exception {
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
        signature.initVerify(publicKey);
        signature.update(data.getBytes(StandardCharsets.UTF_8));
        byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64);
        boolean isValid = signature.verify(signatureBytes);
        logger.debug("簽名驗證結果: {}", isValid ? "通過" : "失敗");
        return isValid;
    }

    /**
     * 將私鑰轉換為PEM格式字串
     */
    public String privateKeyToPem(PrivateKey privateKey) {
        String encoded = Base64.getEncoder().encodeToString(privateKey.getEncoded());
        return "-----BEGIN PRIVATE KEY-----\n" +
                formatBase64String(encoded) +
                "\n-----END PRIVATE KEY-----";
    }

    /**
     * 將公鑰轉換為PEM格式字串
     */
    public String publicKeyToPem(PublicKey publicKey) {
        String encoded = Base64.getEncoder().encodeToString(publicKey.getEncoded());
        return "-----BEGIN PUBLIC KEY-----\n" +
                formatBase64String(encoded) +
                "\n-----END PUBLIC KEY-----";
    }

    /**
     * 從PEM格式字串加載私鑰
     */
    public PrivateKey loadPrivateKeyFromPem(String pemContent) throws Exception {
        String privateKeyPEM = pemContent
                .replaceAll("-----\\w+ PRIVATE KEY-----", "")
                .replaceAll("\\s", "");

        byte[] decoded = Base64.getDecoder().decode(privateKeyPEM);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded);
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        return keyFactory.generatePrivate(spec);
    }

    /**
     * 從PEM格式字串加載公鑰
     */
    public PublicKey loadPublicKeyFromPem(String pemContent) throws Exception {
        String publicKeyPEM = pemContent
                .replaceAll("-----\\w+ PUBLIC KEY-----", "")
                .replaceAll("\\s", "");

        byte[] decoded = Base64.getDecoder().decode(publicKeyPEM);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        return keyFactory.generatePublic(spec);
    }

    /**
     * 格式化Base64字串,每64個字元換行
     */
    private String formatBase64String(String base64) {
        StringBuilder formatted = new StringBuilder();
        for (int i = 0; i < base64.length(); i += 64) {
            formatted.append(base64, i, Math.min(i + 64, base64.length())).append("\n");
        }
        return formatted.toString().trim();
    }
}

許可證資料模型

許可證實體類定義了許可證包含的所有資訊欄位,使用Jackson註解控制JSON序列化順序。

@JsonPropertyOrder({"subject", "issuedTo", "hardwareId", "expireAt", "features"})
public class License {
    private String subject;        // 軟體產品名稱
    private String issuedTo;       // 許可證授權對象
    private String hardwareId;     // 綁定的硬體指紋

    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate expireAt;    // 許可證到期時間

    private List<String> features;  // 授權的功能模組列表
    private String signature;        // 數位簽名(序列化時排除)

    public License() {}

    public License(String subject, String issuedTo, String hardwareId,
                   LocalDate expireAt, List<String> features) {
        this.subject = subject;
        this.issuedTo = issuedTo;
        this.hardwareId = hardwareId;
        this.expireAt = expireAt;
        this.features = features;
    }

    // 完整的getter和setter方法
    public String getSubject() { return subject; }
    public void setSubject(String subject) { this.subject = subject; }

    public String getIssuedTo() { return issuedTo; }
    public void setIssuedTo(String issuedTo) { this.issuedTo = issuedTo; }

    public String getHardwareId() { return hardwareId; }
    public void setHardwareId(String hardwareId) { this.hardwareId = hardwareId; }

    public LocalDate getExpireAt() { return expireAt; }
    public void setExpireAt(LocalDate expireAt) { this.expireAt = expireAt; }

    public List<String> getFeatures() { return features; }
    public void setFeatures(List<String> features) { this.features = features; }

    public String getSignature() { return signature; }
    public void setSignature(String signature) { this.signature = signature; }

    @Override
    public String toString() {
        return "License{" +
                "subject='" + subject + '\'' +
                ", issuedTo='" + issuedTo + '\'' +
                ", hardwareId='" + hardwareId + '\'' +
                ", expireAt=" + expireAt +
                ", features=" + features +
                '}';
    }
}

許可證服務核心邏輯

許可證服務是整個系統的業務核心,負責許可證的生成和驗證邏輯。

@Service
public class LicenseService {
    private static final Logger logger = LoggerFactory.getLogger(LicenseService.class);

    @Autowired
    private RSAUtil rsaUtil;

    @Autowired
    private HardwareUtil hardwareUtil;

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 生成許可證文件
     */
    public String generateLicense(License license, PrivateKey privateKey) throws Exception {
        // 自動填充硬體指紋
        if (license.getHardwareId() == null || license.getHardwareId().isEmpty()) {
            String hardwareId = hardwareUtil.getMotherboardSerial();
            license.setHardwareId(hardwareId);
            logger.info("自動獲取硬體指紋: {}", hardwareId);
        }

        // 創建標準化的JSON資料用於簽名
        String licenseData = createStandardizedLicenseJson(license);
        logger.debug("待簽名的許可證資料: {}", licenseData);

        // 使用私鑰對許可證資料進行簽名
        String signature = rsaUtil.sign(licenseData, privateKey);

        // 創建包含簽名的完整許可證
        JsonNode jsonNode = objectMapper.readTree(licenseData);
        ((ObjectNode) jsonNode).put("signature", signature);

        String result = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode);
        logger.info("許可證生成成功,授權給: {}, 到期時間: {}", license.getIssuedTo(), license.getExpireAt());
        return result;
    }

    /**
     * 驗證許可證的有效性
     */
    public LicenseVerifyResult verifyLicense(String licenseJson, PublicKey publicKey) {
        try {
            JsonNode jsonNode = objectMapper.readTree(licenseJson);

            // 檢查是否包含簽名欄位
            if (!jsonNode.has("signature")) {
                return new LicenseVerifyResult(false, "許可證缺少數位簽名");
            }
            String signature = jsonNode.get("signature").asText();

            // 移除簽名欄位,重構原始許可證資料
            ((ObjectNode) jsonNode).remove("signature");
            License license = objectMapper.readValue(jsonNode.toString(), License.class);

            // 重新生成標準化的JSON資料用於驗證
            String licenseData = createStandardizedLicenseJson(license);
            logger.debug("驗證用許可證資料: {}", licenseData);

            // 驗證數位簽名
            boolean signatureValid = rsaUtil.verify(licenseData, signature, publicKey);
            if (!signatureValid) {
                logger.warn("許可證數位簽名驗證失敗");
                return new LicenseVerifyResult(false, "許可證數位簽名無效");
            }

            // 驗證硬體指紋
            String currentHardwareId = hardwareUtil.getMotherboardSerial();
            if (!currentHardwareId.equals(license.getHardwareId())) {
                logger.warn("硬體指紋不匹配 - 期望: {}, 實際: {}", license.getHardwareId(), currentHardwareId);
                return new LicenseVerifyResult(false,
                        String.format("硬體指紋不匹配。許可證綁定設備: %s, 當前設備: %s",
                                license.getHardwareId(), currentHardwareId));
            }

            // 驗證許可證有效期
            if (license.getExpireAt().isBefore(LocalDate.now())) {
                logger.warn("許可證已過期 - 到期時間: {}, 當前時間: {}", license.getExpireAt(), LocalDate.now());
                return new LicenseVerifyResult(false,
                        String.format("許可證已過期。到期時間: %s, 當前時間: %s",
                                license.getExpireAt(), LocalDate.now()));
            }

            logger.info("許可證驗證通過 - 授權對象: {}, 功能權限: {}", license.getIssuedTo(), license.getFeatures());
            return new LicenseVerifyResult(true, "許可證驗證成功", license);

        } catch (Exception e) {
            logger.error("許可證驗證過程發生異常", e);
            return new LicenseVerifyResult(false, "許可證格式錯誤: " + e.getMessage());
        }
    }

    /**
     * 創建標準化的許可證JSON資料
     * 手動構建JSON確保欄位順序一致,這是簽名驗證成功的關鍵
     */
    private String createStandardizedLicenseJson(License license) throws Exception {
        StringBuilder json = new StringBuilder();
        json.append("{");
        json.append("\"subject\":\"").append(escapeJson(license.getSubject())).append("\",");
        json.append("\"issuedTo\":\"").append(escapeJson(license.getIssuedTo())).append("\",");
        json.append("\"hardwareId\":\"").append(escapeJson(license.getHardwareId())).append("\",");
        json.append("\"expireAt\":\"").append(license.getExpireAt().toString()).append("\",");
        json.append("\"features\":[");

        if (license.getFeatures() != null && !license.getFeatures().isEmpty()) {
            for (int i = 0; i < license.getFeatures().size(); i++) {
                if (i > 0) json.append(",");
                json.append("\"").append(escapeJson(license.getFeatures().get(i))).append("\"");
            }
        }

        json.append("]}");
        return json.toString();
    }

    /**
     * 轉義JSON字串中的特殊字元
     */
    private String escapeJson(String str) {
        if (str == null) return "";
        return str.replace("\\", "\\\\")
                .replace("\"", "\\\"")
                .replace("\n", "\\n")
                .replace("\r", "\\r")
                .replace("\t", "\\t");
    }

    /**
     * 許可證驗證結果封裝類
     */
    public static class LicenseVerifyResult {
        private final boolean valid;
        private final String message;
        private final License license;

        public LicenseVerifyResult(boolean valid, String message) {
            this(valid, message, null);
        }

        public LicenseVerifyResult(boolean valid, String message, License license) {
            this.valid = valid;
            this.message = message;
            this.license = license;
        }

        public boolean isValid() { return valid; }
        public String getMessage() { return message; }
        public License getLicense() { return license; }
    }
}

REST API介面設計

為了提供完整的許可證管理功能,設計了一套RESTful API介面。

@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*")
public class LicenseController {
    private static final Logger logger = LoggerFactory.getLogger(LicenseController.class);

    @Autowired
    private LicenseService licenseService;

    @Autowired
    private KeyManagementService keyManagementService;

    @Autowired
    private HardwareUtil hardwareUtil;

    /**
     * 生成新的RSA密鑰對
     */
    @PostMapping("/keys/generate")
    public ResponseEntity<Map<String, Object>> generateKeys() {
        try {
            Map<String, String> keys = keyManagementService.generateKeyPair();
            Map<String, Object> response = new HashMap<>();
            response.put("success", true);
            response.put("data", keys);
            response.put("message", "密鑰對生成成功");

            logger.info("新的RSA密鑰對生成成功");
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            logger.error("密鑰生成失敗", e);
            Map<String, Object> response = new HashMap<>();
            response.put("success", false);
            response.put("message", "密鑰生成失敗: " + e.getMessage());
            return ResponseEntity.badRequest().body(response);
        }
    }

    /**
     * 加載RSA密鑰
     */
    @PostMapping("/keys/load")
    public ResponseEntity<Map<String, Object>> loadKeys(@RequestBody Map<String, String> request) {
        try {
            String privateKey = request.get("privateKey");
            String publicKey = request.get("publicKey");

            if (privateKey != null && !privateKey.trim().isEmpty()) {
                keyManagementService.loadPrivateKey(privateKey);
                logger.info("私鑰加載成功");
            }

            if (publicKey != null && !publicKey.trim().isEmpty()) {
                keyManagementService.loadPublicKey(publicKey);
                logger.info("公鑰加載成功");
            }

            Map<String, Object> response = new HashMap<>();
            response.put("success", true);
            response.put("message", "密鑰加載成功");
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            logger.error("密鑰加載失敗", e);
            Map<String, Object> response = new HashMap<>();
            response.put("success", false);
            response.put("message", "密鑰加載失敗: " + e.getMessage());
            return ResponseEntity.badRequest().body(response);
        }
    }

    /**
     * 生成許可證
     */
    @PostMapping("/license/generate")
    public ResponseEntity<Map<String, Object>> generateLicense(@RequestBody License license) {
        try {
            if (!keyManagementService.isKeysLoaded()) {
                Map<String, Object> response = new HashMap<>();
                response.put("success", false);
                response.put("message", "請先生成或加載RSA密鑰");
                return ResponseEntity.badRequest().body(response);
            }

            String licenseJson = licenseService.generateLicense(
                license, keyManagementService.getCachedPrivateKey());

            Map<String, Object> response = new HashMap<>();
            response.put("success", true);
            response.put("data", licenseJson);
            response.put("message", "許可證生成成功");

            logger.info("為 {} 生成許可證成功", license.getIssuedTo());
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            logger.error("許可證生成失敗", e);
            Map<String, Object> response = new HashMap<>();
            response.put("success", false);
            response.put("message", "許可證生成失敗: " + e.getMessage());
            return ResponseEntity.badRequest().body(response);
        }
    }

    /**
     * 驗證許可證
     */
    @PostMapping("/license/verify")
    public ResponseEntity<Map<String, Object>> verifyLicense(@RequestBody Map<String, String> request) {
        try {
            String licenseJson = request.get("licenseJson");

            if (licenseJson == null || licenseJson.trim().isEmpty()) {
                Map<String, Object> response = new HashMap<>();
                response.put("success", false);
                response.put("message", "許可證內容不能為空");
                return ResponseEntity.badRequest().body(response);
            }

            if (!keyManagementService.isKeysLoaded()) {
                Map<String, Object> response = new HashMap<>();
                response.put("success", false);
                response.put("message", "請先加載公鑰");
                return ResponseEntity.badRequest().body(response);
            }

            LicenseService.LicenseVerifyResult result = licenseService.verifyLicense(
                licenseJson, keyManagementService.getCachedPublicKey());

            Map<String, Object> response = new HashMap<>();
            response.put("success", result.isValid());
            response.put("message", result.getMessage());
            if (result.getLicense() != null) {
                response.put("license", result.getLicense());
            }

            logger.info("許可證驗證完成,結果: {}", result.isValid() ? "通過" : "失敗");
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            logger.error("許可證驗證過程異常", e);
            Map<String, Object> response = new HashMap<>();
            response.put("success", false);
            response.put("message", "許可證驗證失敗: " + e.getMessage());
            return ResponseEntity.badRequest().body(response);
        }
    }

    /**
     * 獲取當前硬體資訊
     */
    @GetMapping("/hardware/info")
    public ResponseEntity<Map<String, Object>> getHardwareInfo() {
        Map<String, Object> response = new HashMap<>();
        response.put("success", true);

        Map<String, String> hardwareInfo = new HashMap<>();
        hardwareInfo.put("motherboardSerial", hardwareUtil.getMotherboardSerial());
        hardwareInfo.put("systemInfo", hardwareUtil.getSystemInfo());
        hardwareInfo.put("osName", System.getProperty("os.name"));
        hardwareInfo.put("osVersion", System.getProperty("os.version"));
        hardwareInfo.put("osArch", System.getProperty("os.arch"));

        response.put("data", hardwareInfo);
        logger.debug("硬體資訊查詢完成");
        return ResponseEntity.ok(response);
    }

    /**
     * 檢查密鑰加載狀態
     */
    @GetMapping("/keys/status")
    public ResponseEntity<Map<String, Object>> getKeysStatus() {
        Map<String, Object> response = new HashMap<>();
        response.put("success", true);
        response.put("keysLoaded", keyManagementService.isKeysLoaded());
        response.put("hasPrivateKey", keyManagementService.getCachedPrivateKey() != null);
        response.put("hasPublicKey", keyManagementService.getCachedPublicKey() != null);
        return ResponseEntity.ok(response);
    }
}

應用場景與集成

許可證文件格式

生成的許可證是一個標準的JSON文件,包含所有必要的授權資訊和數位簽名:

{
  "subject": "企業管理系統",
  "issuedTo": "北京某某科技有限公司",
  "hardwareId": "BFEBFBFF000906E9",
  "expireAt": "2025-12-31",
  "features": ["USER_MANAGEMENT", "REPORT_EXPORT", "DATA_ANALYSIS"],
  "signature": "MEUCIQDxxx...完整的Base64簽名"
}

欄位說明
subject:軟體產品的名稱或標識

issuedTo:許可證的授權對象(通常是公司名稱)

hardwareId:綁定的硬體指紋(主板序列號)

expireAt:許可證的到期日期

features:授權使用的功能模組列表

signature:使用私鑰生成的數位簽名

功能權限控制

基於許可證的功能權限控制可以通過AOP切面和自定義註解實現

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequireFeature {
    /** 需要的功能權限 */
    String value();

    /** 權限不足時的提示資訊 */
    String message() default "功能未授權";
}

@Component
@Aspect
@Order(1)
public class LicenseFeatureAspect {
    private static final Logger logger = LoggerFactory.getLogger(LicenseFeatureAspect.class);

    @Around("@annotation(requireFeature)")
    public Object checkFeaturePermission(ProceedingJoinPoint joinPoint, RequireFeature requireFeature) throws Throwable {
        // 獲取當前許可證資訊
        License currentLicense = LicenseContext.getCurrentLicense();

        if (currentLicense == null) {
            logger.warn("訪問需要授權的功能,但未找到有效許可證: {}", requireFeature.value());
            throw new LicenseException("系統未找到有效許可證,請聯繫管理員");
        }

        // 檢查功能權限
        if (currentLicense.getFeatures() == null ||
            !currentLicense.getFeatures().contains(requireFeature.value())) {

            logger.warn("功能權限不足 - 使用者: {}, 需要權限: {}, 擁有權限: {}",
                    currentLicense.getIssuedTo(),
                    requireFeature.value(),
                    currentLicense.getFeatures());

            throw new LicenseException(requireFeature.message() + ": " + requireFeature.value());
        }

        logger.debug("功能權限驗證通過: {}", requireFeature.value());
        return joinPoint.proceed();
    }
}

// 使用示例
@RestController
@RequestMapping("/api/report")
public class ReportController {

    @GetMapping("/export")
    @RequireFeature("REPORT_EXPORT")
    public ResponseEntity<byte[]> exportReport(@RequestParam String format) {
        // 報表導出功能實現
        byte[] reportData = generateReport(format);

        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Disposition", "attachment; filename=report." + format);

        return ResponseEntity.ok()
                .headers(headers)
                .body(reportData);
    }

    @PostMapping("/schedule")
    @RequireFeature("REPORT_SCHEDULE")
    public ResponseEntity<String> scheduleReport(@RequestBody ScheduleRequest request) {
        // 定時報表功能實現
        scheduleReportJob(request);
        return ResponseEntity.ok("定時報表創建成功");
    }
}

Web管理介面

本DEMO提供了一個完整的Web管理介面,具備以下功能:

硬體資訊展示:即時顯示當前設備的硬體指紋和系統資訊

密鑰管理:生成新的RSA密鑰對,或加載現有密鑰

許可證生成:創建包含各種權限的許可證文件

許可證驗證:驗證許可證的有效性和權限範圍

主要的前端交互邏輯:

// API基礎配置
const API_BASE = 'http://localhost:8080/api';

// 頁面初始化
document.addEventListener('DOMContentLoaded', function() {
    loadHardwareInfo();
    checkKeyStatus();
    setDefaultExpireDate();
});

// 生成許可證
async function generateLicense() {
    try {
        // 收集表單資料
        const licenseData = {
            subject: document.getElementById('subject').value.trim(),
            issuedTo: document.getElementById('issuedTo').value.trim(),
            expireAt: document.getElementById('expireAt').value,
            features: document.getElementById('features').value.trim()
                .split(',').map(f => f.trim()).filter(f => f)
        };

        // 資料驗證
        if (!licenseData.subject || !licenseData.issuedTo || !licenseData.expireAt) {
            showToast('警告', '請填寫所有必填欄位', 'warning');
            return;
        }

        showToast('處理中', '正在生成許可證...', 'info');

        // 調用API生成許可證
        const response = await fetch(`${API_BASE}/license/generate`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(licenseData)
        });

        const result = await response.json();

        if (result.success) {
            document.getElementById('generatedLicense').value = result.data;
            showToast('成功', '許可證生成成功', 'success');
        } else {
            showToast('錯誤', result.message, 'error');
        }
    } catch (error) {
        console.error('生成許可證失敗:', error);
        showToast('錯誤', '網路連接失敗', 'error');
    }
}

// 驗證許可證
async function verifyLicense() {
    try {
        const licenseJson = document.getElementById('licenseToVerify').value.trim();

        if (!licenseJson) {
            showToast('警告', '請輸入許可證內容', 'warning');
            return;
        }

        showToast('處理中', '正在驗證許可證...', 'info');

        const response = await fetch(`${API_BASE}/license/verify`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ licenseJson })
        });

        const result = await response.json();
        displayVerificationResult(result);

    } catch (error) {
        console.error('驗證許可證失敗:', error);
        showToast('錯誤', '驗證過程出錯', 'error');
    }
}

總結

本文介紹了一套基於Spring Boot + RSA2048的許可證控制系統實現方案,包含硬體指紋獲取、數位簽名驗證、功能權限控制等核心功能,支持跨平台部署,提供完整的Web示例介面,適用於企業軟體的商業化授權控制。

github.com/yuboon/java…


原文出處:https://juejin.cn/post/7555738972116598825


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝18   💬9   ❤️5
680
🥈
我愛JS
📝4   💬14   ❤️7
252
🥉
御魂
💬1  
3
#5
2
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付