Spring Boot 開發(fā)者必看:輕松搞定許可證管控,讓項目變現(xiàn)無憂!
在軟件產(chǎn)品逐漸走向商業(yè)化的過程中,許可證管控已經(jīng)成為必不可少的環(huán)節(jié)。無論你是在構(gòu)建企業(yè)級管理系統(tǒng)、獨立桌面應用,還是在線化的 SaaS 服務,都需要一種可靠機制來限制授權范圍、控制功能模塊、以及設置到期時間。
缺少許可證機制,意味著你的軟件隨時可能被非法拷貝或無授權使用,直接威脅到商業(yè)收益與知識產(chǎn)權。而一個設計完善的許可證系統(tǒng)不僅能保證軟件安全,還能提供差異化定價策略,靈活支撐 按時間、按功能的付費模式。
因此,本文將帶你實現(xiàn)一個基于 Spring Boot 3.x + RSA2048 非對稱加密 的許可證管理方案,覆蓋 硬件綁定、權限控制、到期驗證 等核心能力。我們將從整體架構(gòu)、技術選型,到代碼實現(xiàn)逐步拆解,并提供完整的 API 設計,幫助開發(fā)者快速構(gòu)建商業(yè)化必備的管控體系。
系統(tǒng)設計思路
許可證系統(tǒng)基于 非對稱加密,核心邏輯如下:
- 廠商端:使用私鑰對許可證文件進行簽名;
- 客戶端:使用公鑰驗證許可證真實性;
這樣做的優(yōu)勢:
- 安全性高:私鑰僅存儲在廠商側(cè),公鑰即使被泄露也無法偽造許可證;
- 部署靈活:支持 離線校驗,不依賴中央服務器;
- 擴展性強:可以靈活加上 功能點控制、用戶數(shù)限制 等規(guī)則。
技術選型
- 后端棧
Spring Boot 3.x → 快速開發(fā) Web API
Java Security API → 內(nèi)置 RSA 非對稱加密支持
Jackson → JSON 序列化和反序列化
- 前端棧
原生 JavaScript → 保持輕量級
TailwindCSS → 快速構(gòu)建現(xiàn)代化 UI
RESTful API → 前后端標準交互
- 加密算法
RSA2048 → 足夠安全的密鑰長度
SHA256withRSA → 數(shù)字簽名算法
Base64 → 簽名編碼
核心功能實現(xiàn)
以下代碼全部放在 Linux 風格的目錄結(jié)構(gòu)中,例如:
src/main/java/com/icoderoad/license硬件指紋獲取
硬件綁定是防止盜版的第一道防線。我們以主板序列號作為唯一標識:
package com.icoderoad.license.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
@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 getWindowsSerial();
} else if (os.contains("linux")) {
return getLinuxSerial();
} else {
logger.warn("不支持的系統(tǒng): {}", os);
return "UNKNOWN";
}
} catch (Exception e) {
logger.error("獲取主板序列號失敗", e);
return "UNKNOWN";
}
}
private String getWindowsSerial() throws Exception {
Process process = Runtime.getRuntime().exec("wmic baseboard get serialnumber");
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.isEmpty() && !"SerialNumber".equalsIgnoreCase(line)) {
return line;
}
}
}
process.waitFor();
return "UNKNOWN";
}
private String getLinuxSerial() throws Exception {
Process process = Runtime.getRuntime().exec("sudo dmidecode -s baseboard-serial-number");
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
String line = reader.readLine();
if (line != null && !line.contains("Not Specified")) {
return line.trim();
}
}
process.waitFor();
return getLinuxSerialFromSys();
}
private String getLinuxSerialFromSys() {
try {
Process process = Runtime.getRuntime().exec("cat /sys/class/dmi/id/board_serial");
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
String line = reader.readLine();
return (line != null && !line.isEmpty()) ? line.trim() : "UNKNOWN";
}
} catch (Exception e) {
logger.warn("讀取/sys失敗", e);
return "UNKNOWN";
}
}
}關鍵點:
- 多重策略:優(yōu)先
dmidecode,失敗則 fallback 到/sys/class/dmi/id - 異常兜底:保證失敗返回
"UNKNOWN" - 統(tǒng)一編碼:避免中文亂碼
RSA 加密工具類
用于密鑰生成、簽名、驗簽:
package com.icoderoad.license.crypto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.*;
import java.util.Base64;
@Component
public class RSAUtil {
private static final Logger logger = LoggerFactory.getLogger(RSAUtil.class);
private static final String ALGORITHM = "RSA";
private static final String SIGN_ALGO = "SHA256withRSA";
public KeyPair generateKeyPair() throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);
keyGen.initialize(2048);
logger.info("RSA密鑰對生成完成");
return keyGen.generateKeyPair();
}
public String sign(String data, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance(SIGN_ALGO);
signature.initSign(privateKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signature.sign());
}
public boolean verify(String data, String sign, PublicKey publicKey) throws Exception {
Signature signature = Signature.getInstance(SIGN_ALGO);
signature.initVerify(publicKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
return signature.verify(Base64.getDecoder().decode(sign));
}
public PrivateKey loadPrivateKey(String pem) throws Exception {
String key = pem.replaceAll("-----\\w+ PRIVATE KEY-----", "").replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(key);
return KeyFactory.getInstance(ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(decoded));
}
public PublicKey loadPublicKey(String pem) throws Exception {
String key = pem.replaceAll("-----\\w+ PUBLIC KEY-----", "").replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(key);
return KeyFactory.getInstance(ALGORITHM).generatePublic(new X509EncodedKeySpec(decoded));
}
}許可證實體類
package com.icoderoad.license.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import java.time.LocalDate;
import java.util.List;
@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;
// getters, setters, toString...
}許可證服務邏輯
負責 生成許可證 和 驗證許可證:
package com.icoderoad.license.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.icoderoad.license.crypto.RSAUtil;
import com.icoderoad.license.model.License;
import com.icoderoad.license.util.HardwareUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.time.LocalDate;
@Service
public class LicenseService {
private static final Logger logger = LoggerFactory.getLogger(LicenseService.class);
private final RSAUtil rsaUtil;
private final HardwareUtil hardwareUtil;
private final ObjectMapper objectMapper;
public LicenseService(RSAUtil rsaUtil, HardwareUtil hardwareUtil, ObjectMapper objectMapper) {
this.rsaUtil = rsaUtil;
this.hardwareUtil = hardwareUtil;
this.objectMapper = objectMapper;
}
public String generateLicense(License license, PrivateKey privateKey) throws Exception {
if (license.getHardwareId() == null) {
license.setHardwareId(hardwareUtil.getMotherboardSerial());
}
String data = objectMapper.writeValueAsString(license);
String signature = rsaUtil.sign(data, privateKey);
ObjectNode node = (ObjectNode) objectMapper.readTree(data);
node.put("signature", signature);
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(node);
}
public boolean verifyLicense(String licenseJson, PublicKey publicKey) {
try {
ObjectNode node = (ObjectNode) objectMapper.readTree(licenseJson);
String signature = node.remove("signature").asText();
License license = objectMapper.treeToValue(node, License.class);
if (!rsaUtil.verify(objectMapper.writeValueAsString(license), signature, publicKey)) {
return false;
}
if (!hardwareUtil.getMotherboardSerial().equals(license.getHardwareId())) {
return false;
}
return !license.getExpireAt().isBefore(LocalDate.now());
} catch (Exception e) {
logger.error("驗證失敗", e);
return false;
}
}
}REST API
package com.icoderoad.license.controller;
import com.icoderoad.license.service.LicenseService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/license")
@CrossOrigin
public class LicenseController {
private final LicenseService licenseService;
public LicenseController(LicenseService licenseService) {
this.licenseService = licenseService;
}
@PostMapping("/generate")
public String generateLicense() {
// 簡化演示,實際需接收請求參數(shù)
return "生成的許可證JSON...";
}
@PostMapping("/verify")
public String verifyLicense(@RequestBody String licenseJson) {
return licenseService.verifyLicense(licenseJson, null) ? "驗證通過" : "驗證失敗";
}
}總結(jié)
通過本文的實現(xiàn),我們構(gòu)建了一個 輕量級、可擴展、安全可靠 的許可證管控系統(tǒng),核心特性包括:
- 非對稱加密保障安全 → 私鑰只存廠商,客戶端僅驗證公鑰
- 硬件指紋綁定 → 防止許可證被隨意拷貝
- 三重驗證機制 → 簽名 + 硬件 + 時間
- 離線驗證能力 → 無需中心化服務器
這種設計既能保護軟件知識產(chǎn)權,又能靈活支持商業(yè)化運營(試用、訂閱、按功能收費)。 對于正計劃將軟件推向市場的開發(fā)者而言,這套方案是構(gòu)建 商業(yè)化閉環(huán) 的堅實基礎。
























