一、簡述
開發的軟體産品在傳遞使用的時候,往往有一段時間的試用期,這期間我們不希望自己的代碼被客戶二次拷貝,這個時候 license 就派上用場了,license 的功能包括設定有效期、綁定 ip、綁定 mac 等。授權方直接生成一個 license 給使用方使用,如果需要延長試用期,也隻需要重新生成一份 license 即可,無需手動修改源代碼。
TrueLicense 是一個開源的證書管理引擎,詳細介紹見 https://truelicense.java.net/
首先介紹下 license 授權機制的原理:
- 生成密鑰對,包含私鑰和公鑰。
- 授權者保留私鑰,使用私鑰對授權資訊諸如使用截止日期,mac 位址等内容生成 license 簽名證書。
- 公鑰給使用者,放在代碼中使用,用于驗證 license 簽名證書是否符合使用條件。
二、生成密鑰對
以下指令在 window cmd 指令視窗執行,注意目前執行目錄,最後生成的密鑰對即在該目錄下:
1、首先要用 KeyTool 工具來生成私匙庫:(-alias别名 -validity 3650 表示10年有效)
keytool -genkey -alias privatekey -keysize 1024 -keystore privateKeys.store -validity 3650
2、然後把私匙庫内的證書導出到一個檔案當中
keytool -export -alias privatekey -file certfile.cer -keystore privateKeys.store
3、然後再把這個證書檔案導入到公匙庫
keytool -import -alias publiccert -file certfile.cer -keystore publicCerts.store
最後生成的檔案 privateKeys.store(私鑰)、publicCerts.store(公鑰)拷貝出來備用。
三、準備工作
首先,我們需要引入 truelicense 的 jar 包,用于實作我們的證書管理。
<dependency>
<groupId>de.schlichtherle.truelicense</groupId>
<artifactId>truelicense-core</artifactId>
<version>1.33</version>
</dependency>
然後,我們建立一個單例模式下的證書管理器。
public class LicenseManagerHolder {
private static volatile LicenseManager licenseManager = null;
private LicenseManagerHolder() {
}
public static LicenseManager getLicenseManager(LicenseParam param) {
if (licenseManager == null) {
synchronized (LicenseManagerHolder.class) {
if (licenseManager == null) {
licenseManager = new LicenseManager(param);
}
}
}
return licenseManager;
}
}
四、利用私鑰生成證書
利用私鑰生成證書,我們需要兩部分内容,一部分是私鑰的配置資訊(私鑰的配置資訊在生成私鑰庫的過程中獲得),一部分是自定義的項目證書資訊。如下展示:
########## 私鑰的配置資訊 ###########
# 私鑰的别名
private.key.alias=privatekey
# privateKeyPwd(該密碼是生成密鑰對的密碼 — 需要妥善保管,不能讓使用者知道)
private.key.pwd=123456
# keyStorePwd(該密碼是通路密鑰庫的密碼 — 使用 keytool 生成密鑰對時設定,使用者知道該密碼)
key.store.pwd=123456
# 項目的唯一識别碼
subject=demo
# 密鑰庫的位址(放在 resource 目錄下)
priPath=/privateKeys.store
########## license content ###########
# 釋出日期
issuedTime=2019-09-12
# 有效開始日期
notBefore=2019-09-12
# 有效截止日期
notAfter=2019-12-30
# ip 位址
ipAddress=192.168.31.25
# mac 位址
macAddress=5C-C5-D4-3E-CA-A6
# 使用者類型,使用者(user)、電腦(computer)、其他(else)
consumerType=user
# 證書允許使用的消費者數量
consumerAmount=1
# 證書說明
info=power by xiamen yungu
#生成證書的位址
licPath=D:\\license.lic
接下來,就是如何生成證書的實操部分了
@Slf4j
public class CreateLicense {
/**
* X500Princal 是一個證書檔案的固有格式,詳見API
*/
private final static X500Principal DEFAULT_HOLDERAND_ISSUER = new X500Principal("CN=Duke, OU=JavaSoft, O=Sun Microsystems, C=US");
private String priAlias;
private String privateKeyPwd;
private String keyStorePwd;
private String subject;
private String priPath;
private String issued;
private String notBefore;
private String notAfter;
private String ipAddress;
private String macAddress;
private String consumerType;
private int consumerAmount;
private String info;
private String licPath;
/**
* 構造器,參數初始化
*
* @param confPath 參數配置檔案路徑
*/
public CreateLicense(String confPath) {
// 擷取參數
Properties prop = new Properties();
try (InputStream in = getClass().getResourceAsStream(confPath)) {
prop.load(in);
} catch (IOException e) {
log.error("CreateLicense Properties load inputStream error.", e);
}
//common param
priAlias = prop.getProperty("private.key.alias");
privateKeyPwd = prop.getProperty("private.key.pwd");
keyStorePwd = prop.getProperty("key.store.pwd");
subject = prop.getProperty("subject");
priPath = prop.getProperty("priPath");
// license content
issued = prop.getProperty("issuedTime");
notBefore = prop.getProperty("notBefore");
notAfter = prop.getProperty("notAfter");
ipAddress = prop.getProperty("ipAddress");
macAddress = prop.getProperty("macAddress");
consumerType = prop.getProperty("consumerType");
consumerAmount = Integer.valueOf(prop.getProperty("consumerAmount"));
info = prop.getProperty("info");
licPath = prop.getProperty("licPath");
}
/**
* 生成證書,在證書釋出者端執行
*
* @throws Exception
*/
public void create() throws Exception {
LicenseManager licenseManager = LicenseManagerHolder.getLicenseManager(initLicenseParams());
licenseManager.store(buildLicenseContent(), new File(licPath));
log.info("------ 證書釋出成功 ------");
}
/**
* 初始化證書的相關參數
*
* @return
*/
private LicenseParam initLicenseParams() {
Class<CreateLicense> clazz = CreateLicense.class;
Preferences preferences = Preferences.userNodeForPackage(clazz);
// 設定對證書内容加密的對稱密碼
CipherParam cipherParam = new DefaultCipherParam(keyStorePwd);
// 參數 1,2 從哪個Class.getResource()獲得密鑰庫;
// 參數 3 密鑰庫的别名;
// 參數 4 密鑰庫存儲密碼;
// 參數 5 密鑰庫密碼
KeyStoreParam privateStoreParam = new DefaultKeyStoreParam(clazz, priPath, priAlias, keyStorePwd, privateKeyPwd);
// 傳回生成證書時需要的參數
return new DefaultLicenseParam(subject, preferences, privateStoreParam, cipherParam);
}
/**
* 通過外部配置檔案建構證書的的相關資訊
*
* @return
* @throws ParseException
*/
public LicenseContent buildLicenseContent() throws ParseException {
LicenseContent content = new LicenseContent();
SimpleDateFormat formate = new SimpleDateFormat("yyyy-MM-dd");
content.setConsumerAmount(consumerAmount);
content.setConsumerType(consumerType);
content.setHolder(DEFAULT_HOLDERAND_ISSUER);
content.setIssuer(DEFAULT_HOLDERAND_ISSUER);
content.setIssued(formate.parse(issued));
content.setNotBefore(formate.parse(notBefore));
content.setNotAfter(formate.parse(notAfter));
content.setInfo(info);
// 擴充字段
Map<String, String> map = new HashMap<>(4);
map.put("ip", ipAddress);
map.put("mac", macAddress);
content.setExtra(map);
return content;
}
}
最後,來嘗試生成一份證書吧!
public static void main(String[] args) throws Exception {
CreateLicense clicense = new CreateLicense("/licenseCreateParam.properties");
clicense.create();
}
四、利用公鑰驗證證書
利用公鑰生成證書,我們需要有公鑰庫、license 證書等資訊。
########## 公鑰的配置資訊 ###########
# 公鑰别名
public.alias=publiccert
# 該密碼是通路密鑰庫的密碼 — 使用 keytool 生成密鑰對時設定,使用者知道該密碼
key.store.pwd=123456
# 項目的唯一識别碼 — 和私鑰的 subject 保持一緻
subject = yungu
# 證書路徑(我這邊配置在了 linux 根路徑下,即 /license.lic )
license.dir=/license.lic
# 公共庫路徑(放在 resource 目錄下)
public.store.path=/publicCerts.store
接下來就是怎麼用公鑰驗證 license 證書,怎樣驗證 ip、mac 位址等資訊的過程了~
@Slf4j
public class VerifyLicense {
private String pubAlias;
private String keyStorePwd;
private String subject;
private String licDir;
private String pubPath;
public VerifyLicense() {
// 取預設配置
setConf("/licenseVerifyParam.properties");
}
public VerifyLicense(String confPath) {
setConf(confPath);
}
/**
* 通過外部配置檔案擷取配置資訊
*
* @param confPath 配置檔案路徑
*/
private void setConf(String confPath) {
// 擷取參數
Properties prop = new Properties();
InputStream in = getClass().getResourceAsStream(confPath);
try {
prop.load(in);
} catch (IOException e) {
log.error("VerifyLicense Properties load inputStream error.", e);
}
this.subject = prop.getProperty("subject");
this.pubAlias = prop.getProperty("public.alias");
this.keyStorePwd = prop.getProperty("key.store.pwd");
this.licDir = prop.getProperty("license.dir");
this.pubPath = prop.getProperty("public.store.path");
}
/**
* 安裝證書證書
*/
public void install() {
try {
LicenseManager licenseManager = getLicenseManager();
licenseManager.install(new File(licDir));
log.info("安裝證書成功!");
} catch (Exception e) {
log.error("安裝證書失敗!", e);
Runtime.getRuntime().halt(1);
}
}
private LicenseManager getLicenseManager() {
return LicenseManagerHolder.getLicenseManager(initLicenseParams());
}
/**
* 初始化證書的相關參數
*/
private LicenseParam initLicenseParams() {
Class<VerifyLicense> clazz = VerifyLicense.class;
Preferences pre = Preferences.userNodeForPackage(clazz);
CipherParam cipherParam = new DefaultCipherParam(keyStorePwd);
KeyStoreParam pubStoreParam = new DefaultKeyStoreParam(clazz, pubPath, pubAlias, keyStorePwd, null);
return new DefaultLicenseParam(subject, pre, pubStoreParam, cipherParam);
}
/**
* 驗證證書的合法性
*/
public boolean vertify() {
try {
LicenseManager licenseManager = getLicenseManager();
LicenseContent verify = licenseManager.verify();
log.info("驗證證書成功!");
Map<String, String> extra = (Map) verify.getExtra();
String ip = extra.get("ip");
InetAddress inetAddress = InetAddress.getLocalHost();
String localIp = inetAddress.toString().split("/")[1];
if (!Objects.equals(ip, localIp)) {
log.error("IP 位址驗證不通過");
return false;
}
String mac = extra.get("mac");
String localMac = getLocalMac(inetAddress);
if (!Objects.equals(mac, localMac)) {
log.error("MAC 位址驗證不通過");
return false;
}
log.info("IP、MAC位址驗證通過");
return true;
} catch (LicenseContentException ex) {
log.error("證書已經過期!", ex);
return false;
} catch (Exception e) {
log.error("驗證證書失敗!", e);
return false;
}
}
/**
* 得到本機 mac 位址
*
* @param inetAddress
* @throws SocketException
*/
private String getLocalMac(InetAddress inetAddress) throws SocketException {
//擷取網卡,擷取位址
byte[] mac = NetworkInterface.getByInetAddress(inetAddress).getHardwareAddress();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < mac.length; i++) {
if (i != 0) {
sb.append("-");
}
//位元組轉換為整數
int temp = mac[i] & 0xff;
String str = Integer.toHexString(temp);
if (str.length() == 1) {
sb.append("0" + str);
} else {
sb.append(str);
}
}
return sb.toString().toUpperCase();
}
}
有了公鑰的驗證過程了,等下!事情還沒結束呢!我們需要在項目啟動的時候,安裝 licnese 證書,然後驗證ip、mac 等資訊。如果校驗不通過,就阻止項目啟動!
@Component
public class LicenseCheck {
@PostConstruct
public void init() {
VerifyLicense vlicense = new VerifyLicense();
vlicense.install();
if (!vlicense.vertify()) {
Runtime.getRuntime().halt(1);
}
}
}