序言
在軟件或產品交付時,我們往往會授權給第三方或者防止程序亂部署而對部署的服務器進行認證,此時License就排上用途了。授權的方便在於如果證書過期,我們可以重新生成一個認證文件而不用修改程序。下面我將講述使用truelicense來實現license的生成和使用。Truelicense是一個開源的證書管理引擎,只不過這個已經不在java官網中維護了,而在http://grepcode.com/project/repo1.maven.org/maven2/de.schlichtherle.truelicense/truelicense-core/這裏。
下面簡單描述license授權機制的原理:
1、生成密鑰對,公鑰和私鑰(私鑰對信息加密,公鑰對信息解密)
2、授權者保留私鑰,使用私鑰對包含授權信息(如使用開始日期,截止日期,IP地址以及MAC地址等)的license進行數字簽名。
3、公鑰以及認證文件給使用者(放在驗證的代碼中使用),用於驗證license是否符合使用條件。
生成密鑰對
java中已經自帶了生成密鑰的工具,就是keytool,此工具的詳細使用可參考一下地址:https://docs.oracle.com/javase/7/docs/technotes/tools/solaris/keytool.html ,其中注意jdk6和jdk7之間的差異,我這用的jdk7的方式,當然jdk6 的指令也可以使用。
生成私鑰庫
windows下進入cmd,Linux進去terminal並執行以下命令(需要安裝jdk7並且keytool要加入到path中,詳細可自行google)
keytool -alias privateKey -genkeypair -keystore privateKeys.keystore -validity 3650 -storepass abc123456 -keypass xyz123456 -dname="test"
簡單說明:
-alias 別名
-genkeypair 生成命令(-genkey jdk6的方式)
-keystore 密鑰庫
-validity 有效期 (單位:天)
-storepass 存儲密碼
-keypass 密鑰密碼
-dname 描述信息
按照提示完成此命令則會在當前目錄下生成一個privateKeys.keystore文件
私匙庫的證書導出到一個文件
keytool -export -alias privateKey -file certfile.cer -keystore privateKeys.keystore
把證書文件導入到公匙庫
keytool -import -alias publicCert -file certfile.cer -keystore publicCerts.keystore
這樣我們就生成了兩個文件privateKey.keystore和publicCert.keystore,一個是私鑰一個公鑰。
程序開發
創建maven工程
我使用的idea,創建一個maven工程在pom文件中添加一下jar包依賴
<dependencies>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>de.schlichtherle.truelicense</groupId>
<artifactId>truelicense-core</artifactId>
<version>1.33</version>
</dependency>
<dependency>
<groupId>de.schlichtherle.truelicense</groupId>
<artifactId>truelicense-xml</artifactId>
<version>1.33</version>
</dependency>
<dependency>
<groupId>de.schlichtherle.truelicense</groupId>
<artifactId>truelicense-swing</artifactId>
<version>1.33</version>
</dependency>
</dependencies>
另外把生成的私鑰密鑰文件放入到classpath下面。
生成證書
生成證書主要使用了LicenseManager類,我們先創建一個單例的實例來用,代碼如下:
public class LicenseManagerHolder {
private static LicenseManager licenseManager;
public static synchronized LicenseManager getLicenseManager(LicenseParam licenseParams) {
if (licenseManager == null) {
licenseManager = new LicenseManager(licenseParams);
}
return licenseManager;
}
}
接下來就可以創建生成證書的類,CreateLicense.java代碼如下:
package com.test.license;
import de.schlichtherle.license.*;
import javax.security.auth.x500.X500Principal;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Properties;
import java.util.prefs.Preferences;
/**
* 證書生成類
*/
public class CreateLicense {
/**
* common param
*/
private static String PRIVATE_ALIAS = "";
private static String KEY_PWD = "";
private static String STORE_PWD = "";
private static String SUBJECT = "";
private static String licPath = "";
private static String priPath = "";
/**
* license content
*/
private static String issuedTime = "";
private static String notBefore = "";
private static String notAfter = "";
private static String consumerType = "";
private static int consumerAmount = 0;
private static String info = "";
// 爲了方便直接用的API裏的例子
// X500Princal是一個證書文件的固有格式,詳見API
private final static X500Principal DEFAULT_HOLDER_AND_ISSUER = new X500Principal("CN=Duke、OU=JavaSoft、O=Sun Microsystems、C=US");
/**
* 讀取配置文件數據
*/
public void setParam(String propertiesPath) {
// 獲取參數
Properties prop = new Properties();
InputStream in = CreateLicense.class.getClassLoader().getResourceAsStream(propertiesPath);
try {
prop.load(in);
} catch (IOException e) {
e.printStackTrace();
}
PRIVATE_ALIAS = prop.getProperty("PRIVATEALIAS");
KEY_PWD = prop.getProperty("KEYPWD");
STORE_PWD = prop.getProperty("STOREPWD");
SUBJECT = prop.getProperty("SUBJECT");
licPath = prop.getProperty("licPath");
priPath = prop.getProperty("priPath");
//license content
issuedTime = prop.getProperty("issuedTime");
notBefore = prop.getProperty("notBefore");
notAfter = prop.getProperty("notAfter");
consumerType = prop.getProperty("consumerType");
consumerAmount = Integer.valueOf(prop.getProperty("consumerAmount"));
info = prop.getProperty("info");
}
/**
* 證書發佈者端執行
*/
public boolean create() {
try {
LicenseManager licenseManager = LicenseManagerHolder
.getLicenseManager(initLicenseParams0());
LicenseContent content = createLicenseContent();
licenseManager.store(content, new File(licPath));
} catch (Exception e) {
e.printStackTrace();
System.out.println("客戶端證書生成失敗!");
return false;
}
System.out.println("服務器端生成證書成功!");
return true;
}
// 返回生成證書時需要的參數
private static LicenseParam initLicenseParams0() {
Preferences preference = Preferences
.userNodeForPackage(CreateLicense.class);
// 設置對證書內容加密的對稱密碼
CipherParam cipherParam = new DefaultCipherParam(STORE_PWD);
// 參數1,2從Class.getResource()獲得密鑰庫;
// 參數3密鑰庫的別名;
// 參數4密鑰庫存儲密碼;
// 參數5密鑰庫密碼
KeyStoreParam privateStoreParam = new DefaultKeyStoreParam(
CreateLicense.class, priPath, PRIVATE_ALIAS, STORE_PWD, KEY_PWD);
LicenseParam licenseParams = new DefaultLicenseParam(SUBJECT,
preference, privateStoreParam, cipherParam);
return licenseParams;
}
// 從外部表單拿到證書的內容
public final static LicenseContent createLicenseContent() {
DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
LicenseContent content = null;
content = new LicenseContent();
content.setSubject(SUBJECT);
content.setHolder(DEFAULT_HOLDER_AND_ISSUER);
content.setIssuer(DEFAULT_HOLDER_AND_ISSUER);
try {
content.setIssued(format.parse(issuedTime));
content.setNotBefore(format.parse(notBefore));
content.setNotAfter(format.parse(notAfter));
} catch (ParseException e) {
e.printStackTrace();
}
content.setConsumerType(consumerType);
content.setConsumerAmount(consumerAmount);
content.setInfo(info);
// 擴展,後期可添加自定義數據,比如校驗ip和mac地址等。
content.setExtra(new Object());
return content;
}
}
送上配置文件配置數據:
##########common parameters###########
#alias
PRIVATEALIAS=privateKey
#key(該密碼生成密鑰對的密碼,需要妥善保管,不能讓使用者知道)
KEYPWD=xyz123456
#STOREPWD(該密碼是在使用keytool生成密鑰對時設置的密鑰庫的訪問密碼)
STOREPWD=abc123456
#SUBJECT
SUBJECT=test
#licPath
licPath=test.lic
#priPath放入到classpath下,按照以下方式讀取,文件名前加"/"
priPath=/privateKeys.keystore
##########license content###########
#issuedTime 發佈時間
issuedTime=2017-05-01
#notBeforeTime 在xx之前不生效
notBefore=2017-05-01
#notAfterTime 在xx之後生效
notAfter=2019-11-29
#consumerType
consumerType=user
#ConsumerAmount
consumerAmount=1
#info 說明信息
info=this is a license
下面我們就可以測試了,代碼如下:
package com.test.license;
public class LicenseTest {
public static void main(String[] args) {
CreateLicense cLicense = new CreateLicense();
//獲取參數
cLicense.setParam("createparam.properties");
//生成證書
cLicense.create();
}
}
這樣就會在classpath下生成一個test.lic的證書,這個證書就是要給三方提供的文件。
證書驗證
創建證書認證類,如下:
package com.test.license;
import de.schlichtherle.license.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Properties;
import java.util.prefs.Preferences;
/**
* 證書驗證類.
*/
public class VerifyLicense {
//common param
private static String PUBLICALIAS = "";
private static String STOREPWD = "";
private static String SUBJECT = "";
private static String licPath = "";
private static String pubPath = "";
public void setParam(String propertiesPath) {
// 獲取參數
Properties prop = new Properties();
InputStream in = VerifyLicense.class.getClassLoader().getResourceAsStream(propertiesPath);
try {
prop.load(in);
} catch (IOException e) {
e.printStackTrace();
}
PUBLICALIAS = prop.getProperty("PUBLICALIAS");
STOREPWD = prop.getProperty("STOREPWD");
SUBJECT = prop.getProperty("SUBJECT");
licPath = prop.getProperty("licPath");
pubPath = prop.getProperty("pubPath");
}
public boolean verify() {
/************** 證書使用者端執行 ******************/
LicenseManager licenseManager = LicenseManagerHolder
.getLicenseManager(initLicenseParams());
// 安裝證書
try {
licenseManager.uninstall();
URL url = VerifyLicense.class.getClassLoader().getResource(licPath);
System.out.println(url.getFile());
licenseManager.install(new File(url.getFile()));
System.out.println("客戶端安裝證書成功!");
} catch (Exception e) {
e.printStackTrace();
System.out.println("客戶端證書安裝失敗!");
return false;
}
// 驗證證書
try {
licenseManager.verify();
System.out.println("客戶端驗證證書成功!");
} catch (Exception e) {
e.printStackTrace();
System.out.println("客戶端證書驗證失效!");
return false;
}
return true;
}
// 返回驗證證書需要的參數
private static LicenseParam initLicenseParams() {
Preferences preference = Preferences
.userNodeForPackage(VerifyLicense.class);
CipherParam cipherParam = new DefaultCipherParam(STOREPWD);
KeyStoreParam privateStoreParam = new DefaultKeyStoreParam(
VerifyLicense.class, pubPath, PUBLICALIAS, STOREPWD, null);
LicenseParam licenseParams = new DefaultLicenseParam(SUBJECT,
preference, privateStoreParam, cipherParam);
return licenseParams;
}
}
測試類如下:
package com.test.license;
public class LicenseVerifyTest {
public static void main(String[] args) {
VerifyLicense vLicense = new VerifyLicense();
//獲取參數
vLicense.setParam("verifyparam.properties");
//驗證證書
vLicense.verify();
}
}
以下是配置文件:
##########common parameters###########
#alias
PUBLICALIAS=publicCert
#STOREPWD(該密碼是在使用keytool生成密鑰對時設置的密鑰庫的訪問密碼)
STOREPWD=abc123456
#SUBJECT
SUBJECT=test
#licPath
licPath=test.lic
#pubPath
pubPath=/publicCerts.keystore
總結
到此,關於java的證書生成以及代碼實現全部搞定,但是我發現這個三方庫暫時只支持SHA1withDSA這個算法,如果我們在使用keytool -keyalg RSA 算法時,這個工具是不支持的,要支持就得手動修改其源碼,具體的後續再詳細記錄一下。