在深入介紹JSSE之前,讓我們來一個簡單的客戶機/服務器程序,程序中包含了兩個文件:SimpleSSLServer和SimpleSSLClient。在運行程序之前,你需要配置下面這些KeyStore和TrestStore文件: 輸入keystore密碼: password (如果和 keystore 密碼相同,按回車): keytool -export -alias alice -keystore clientKeys -file alice.cer keytool -export -alias bob -keystore clientKeys -file bob.cer 這樣keytool就在當前目錄下創建了三個授權文件。然后我們將server.cer文件導入到clientTrust文件中;將alice.cer和bob.cer導入到serverTruest文件中: keytool -import -alias server -keystore clientTrust -file server.cer 然后在另一個命令窗口中運行客戶端程序: Connected SimpleSSLServer 讓我們先來看一下SimpleSSLServer。在main()方法中,程序獲得了缺省的SSLServerSocketFactory對象;然后利用SSLServerSocketFactory創建一個SimpleSSLServer對象,最后調用start()方法啟動SimpleSSLServer對象。 SSLServerSocketFactory ssf= SimpleSSLClient SimpleSSLClient類比較簡單,但是在后面的一些比較復雜的例子中的類會繼承該類。在getSLLSocketFactory()方法中,程序返回缺省的工廠類: protected SSLSocketFactory getSSLSocketFactory() boolean done=false; 在下面的例子中,我們將使用CustomTrustStoreClient來動態定義KeyStore和TrustStore。首先讓我們先運行一下CustomTrustStoreClient: java CustomTrustStoreClient protected KeyManager[] getKeyManagers() 當運行前幾個例子的時候,不知道大家是否注意到服務器端顯示的授權的標識名稱。在前面我們授權給了兩個人:Alice和Bob,在運行程序時JSSE會從中任選一個。在我的計算機上JSSE選擇的總是Bob,或許在你的計算機上情況會有所不同。下面讓我們來看一看最后一個例子程序:SelectAliasClient。這個例子使你能夠在運行客戶端時使用指定的授權。例如你需要指定使用Alice的授權,由于Alice的別名是alice,你需要在命令窗口中鍵入下面的命令: java SelectAliasClient -alias alice 1.JSSE調用chooseClientAlias()方法獲得指定的授權。 2.chooseClientAlias()方法調用X509KeyManager接口的getClientAlaises()方法獲得SSLSocket對象使用的所有授權的別名,然后檢查指定的授權別名是否有效。 public String chooseClientAlias(String[] keyType, Principal[] issuers, String[] validAliases=baseKM.getClientAliases(keyType, issuers); if (validAliases[j].equals(alias)) aliasFound=true; 然后我們就可以在程序中用AliasForingKeyManager對象來替代KeyManager對象了。在getSSLSocketFactory()方法中,我們只需要將通過調用getKeyManagers()方法獲得KeyManager對象數組,然后將其強制轉化為AliasForcingKeyManager對象就可以了。下面是新的getSSLSocketFactory()方法的代碼: protected SSLSocketFactory getSSLSocketFactory() // 這里只處理了X509KeyManager接口 · 使用HandshagCompletedListerner對象來獲得關于連接的信息。 · 從SSLContext對象中獲得一個SLLSocketFactory對象。 · 使用動態的TrustStroe或KeyStore。 · 通過實現自己的KeyManager類來指定JSSE使用的授權。 如果大家有興趣的話,還可以進一步將這些技術進行擴展。例如你可以在JSSE的其他類中使用X509KeyManager接口,也可以在TrustStore和KeyStore的實現類中從數據庫中讀取授權信息。但是在使用自己編寫的TrustStore,KeyStore,TrustManager和KeyManager的時候,需要非常小心,因為任何一個細微的錯誤都可能導致SSL連接不再是安全的了。
· 一個客戶端的KeyStore文件,該文件中包含了對Alice和Bob的授權。
· 一個服務器端的KeyStore文件,該文件中包含了對server的授權。
· 一個名為clientTrust的客戶端TrustStore文件,該文件中包含了對server的授權。
· 一個名為serverTrust的服務器端TrustStore文件,該文件中包含了對Alice和Bob的授權。
使用keytool可以幫助你創建這些文件(該工具在Java的bin目錄下):
· 一個客戶端的KeyStore文件,該文件中包含了對Alice和Bob的授權。
在命令窗口中輸入下面的命令:
keytool -genkey -alias alice -keystore clientKeys
窗口中會出現下面的提示,根據提示輸入相應的信息:
您的名字與姓氏是什么?
[Unknown]: Alice
您的組織單位名稱是什么?
[Unknown]: Development
您的組織名稱是什么?
[Unknown]: DCQ
您所在的城市或區域名稱是什么?
[Unknown]: ChongQing
您所在的州或省份名稱是什么?
[Unknown]: ChongQing
該單位的兩字母國家代碼是什么
[Unknown]: CH
CN=Alice, OU=Development, O=DCQ, L=ChongQing, ST=ChongQing, C=CH 正確嗎?
[否]: 是
輸入的主密碼
通過相同的方式可以建立對Bob的授權。
keytool -genkey -alias bob -keystore clientKeys
注意在名字與姓氏一欄中填寫Bob。在完成后可以鍵入下面的命令來檢測是否已經正確完成了授權。
keytool -list -v -keystore clientKeys????
· 一個服務器端的KeyStore文件,該文件中包含了對server的授權。
在命令窗口中鍵入下面的命令:
keytool -genkey -alias server -keystore serverKeys
注意將密碼設為password,名字與姓氏設定為Server。完成授權后同樣可以通過上面提到的命令來檢測。
· 一個名為clientTrust的客戶端TrustStore文件,該文件中包含了對server的授權。以及一個名為serverTrust的服務器端TrustStore文件,該文件中包含了對Alice和Bob的授權。
keytool -export -alias server -keystore clientKeys -file server.cer
輸入keystore密碼: password
保存在文件中的認證
輸入keystore密碼: password
保存在文件中的認證
輸入keystore密碼: password
保存在文件中的認證
keytool -import -alias alice -keystore serverTrust -file alice.cer
keytool -import -alias bob -keystore serverTrust-file bob.cer
到目前為止,在當前目錄下包含clientKeys,serverKeys,clientTrust,serverTrust四個文件。完成了KeyStore和TrustStore的設置后就可以運行例子程序了。首先需要運行服務器程序:
java -Djavax.net.ssl.keyStore=serverKeys
-Djavax.net.ssl.keyStorePassword=password
-Djavax.net.ssl.trustStore#NAME?
-Djavax.net.ssl.trustStorePassword=password SimpleSSLServer
在命令行中我們指定了keyStore屬性為serverKeys。由于服務器程序需要獲得客戶端的授權信息,我們指定trustStore為serverTrust。這樣SSLSimpleServer就可以驗證由SSLSimpleClient提供的授權信息。當服務器程序成功運行后,你會看到下面的提示:
SimpleSSLServer running on port 49152????
這時候服務器會等待客戶端發出建立連接的申請。如果你希望在另一個端口上運行服務器程序,可以在命令中指定-port xxx參數,其中xxx是端口號。
java -Djavax.net.ssl.keyStore=clientKeys
-Djavax.net.ssl.keyStorePassword=password
-Djavax.net.ssl.trustStore=clientTrust
-Djavax.net.ssl.trustStorePassword=password SimpleSSLClient
客戶端程序會試圖向本機的49152端口建立SSL連接。同樣你可以通過-port參數指定端口號,也可以通過-host參數指定主機名稱。當連接成功后,會出現下面的提示信息:
同時在服務器端會提示用戶客戶端已經連接成功。
(SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
SimpleSSLServer server=new SimpleSSLServer(ssf,port);
server.start();
由于服務器是在一個單獨的線程中運行的,main()方法啟動了服務器之后就退出了。start()方法啟動了一個新的線程,該線程執行run()方法中的代碼。在run()方法中創建了一個SSLServerSocket對象,然后設定服務器需要進行客戶端驗證:
SSLServerSocket serverSocket= (SSLServerSocket)serverSocketFactory.createServerSocket(port);
serverSocket.setNeedClientAuth(true);
調用run()方法后,程序進入了一個死循環,等待客戶端的連接申請。循環中的每個Socket對應一個HandshakeCompletedListener對象(該對象是用來顯示客戶驗證信息中的標識名稱[distinguished name]的)。Socket的InputStream對象被包裝在一個InputDisplayer對象中,這個InputDisplayer對象運行在另外一個線程中,用來將Socket接收到的數據發送到System.out。下面的代碼是SimpleSSLServer中的主循環體:
while (true) {
String ident=String.valueOf(id++);
//監聽連接請求.
SSLSocket socket=(SSLSocket)serverSocket.accept();
//通過使用HandshakeCompletedListener對象,程序進行授權驗證.
HandshakeCompletedListener hcl=new SimpleHandshakeListener(ident);
socket.addHandshakeCompletedListener(hcl);
InputStream in=socket.getInputStream();
new InputDisplayer(ident, in);
}
程序中的SimpleHandshakeListener類實現了HandshakeCompletedListerner接口。在SimpleHandshakeListener類中實現了handshakeCompleted()方法,該方法在SSL握手階段完成后將被JSSE調用。它將顯示出客戶端的標識名稱:
class SimpleHandshakeListener implements HandshakeCompletedListener
{
String ident;
/**
* 構造函數.
*/
public SimpleHandshakeListener(String ident)
{
this.ident=ident;
}
/**當SSL握手過程完成后該方法被激活. */
public void handshakeCompleted(HandshakeCompletedEvent event)
{
//顯示授權信息.
try {
X509Certificate
cert=(X509Certificate)event.getPeerCertificates()[0];
String peer=cert.getSubjectDN().getName();
System.out.println(ident+": Request from "+peer);
}
catch (SSLPeerUnverifiedException pue) {
System.out.println(ident+": Peer unverified");
}
}
}
用紅色字體表示的兩行代碼是這段代碼的核心:getPeerCertificates()方法返回一個X509Certificated對象的數組。這些X509Certificated對象創建了客戶端的身份標識。在數組中的第一個元素是客戶端的驗證信息,而最后一個通常是CA驗證。當我們有了客戶端的驗證信息后。我們可以得到其中的標識名稱,并將它傳送到System.out。
throws IOException, GeneralSecurityException
{
return (SSLSocketFactory)SSLSocketFactory.getDefault();
}
在runClient()方法中,程序處理了輸入參數后,獲得SSLSockFactory對象,調用connect()方法連接到服務器程序。在connect()方法中,程序首先創建一個SSLSocket對象,然后調用SSLSocket對象的startHandshang()方法啟動和服務器端的握手過程。當握手過程完成后,會觸發一個HandshakeCompletedEvent事件。在服務器端的HandshakeCompletedListener對象會處理這個事件。事實上,JSSE可以自動啟動握手過程,但是必須是在第一次有數據通過Socket傳輸的情況下。由于在例子程序中,直到用戶在鍵盤上輸入信息后才會有數據通過Socket傳輸,而我們希望服務器端及時報告連接情況,因此我們用startShake()方法來手工激活握手過程。
while (!done) {
String line=reader.readLine();
if (line!=null) {
writer.write(line);
writer.write('\n');
writer.flush();
}
else done=true;
}
定制KeyStore和TrustStore?
還記得我們是如何運行客戶端的嗎?我們需要在命令行中指定keyStore,keyStorePasword, trustStore和trustStorePassword參數,以至于整個命令顯得過于冗長。事實上你可以在程序中指定KeyStore和TrustStore,后面的例子中將告訴你如何實現這一點。同時在例子中還會演示如何配置多個SSLSocketFactory對象,其中每個SSLSocketFactory對象對應不同的KeyStore和TrustStore設置。如果沒有這種技術,在同一個虛擬機上的所有安全連接都只能使用同一個KeyStore和TrustStore。對于比較小的應用程序,這也許不會產生問題;但是對于那些比較大的應用程序來說,這絕對是一個嚴重的缺陷。
為什么運行CustomTrustStoreClient時不需要指定KeyStore和TrustStore參數呢?這是應為在CustomTrustStoreClient的代碼中指定了KeyStore(ClientKeys)和TrustStore(ClientTruts)以及它們的密鑰(password)。如果你想使用其他的KeyStore、 TrustStore或密鑰,可以使用-ks、-kspass、-ts和-tspass參數來指定。下面讓我們來看一下CustomTrustStoreClient的getSSLSocketFactory()方法。該方法通過調用getTrustManager()方法獲得一個TurstManager對象數組,通過調用getKeyManagers()方法獲得一個KeyManager對象數組。然后利用得到的TurstManager和KeyManager對象數組構造一個SSLContext對象,最后通過SSLContext對象的getSocketFactory()方法來配置JSSE。需要注意的是在調用SSLContext類的init()方法時使用的參數。第一個參數是KeyManager對象數組。第二個參數和第一個參數類似,是TrustManager數組。如果前兩個參數被設定為null,程序將使用缺省的KeyManager和TrustStore(缺省的KeyStore來源于系統屬性中的javax.net.ssl.keyStore和javax.net.ssl.keyStorePassword屬性;缺省的TrustStore來源于系統屬性中的javax.net.ssl.trustStore和javax.net.ssl.trustStorePassword屬性)。通過設定第三個參數可以指定JSSE中的隨機數產生器(Random Number Generate, RNG)。由于在SSL中隨機數的產生是一個很敏感的問題,錯誤使用這個參數會導致安全連接變得不安全,因此我在例子中使用了null。這樣程序將使用缺省的并且是安全的SecureRandom對象。
protected SSLSocketFactory getSSLSocketFactory()
throws IOException, GeneralSecurityException
{
// 調用getTrustManagers方法獲得trust managers
TrustManager[] tms=getTrustManagers();
// 調用getKeyManagers方法獲得key manager
KeyManager[] kms=getKeyManagers();
//利用KeyManagers創建一個SSLContext對象.用獲得的KeyStore和
// TrustStore初始化該SSLContext對象.我們使用缺省的SecureRandom.
SSLContext context=SSLContext.getInstance("SSL");
context.init(kms, tms, null);
//最后獲得了SocketFactory對象.
SSLSocketFactory ssf=context.getSocketFactory();
return ssf;
}
下面讓我們看一看CustomKeyStoreClient類中的getKeyMangers()方法是如何初始化KeyManagers對象數組的:
throws IOException, GeneralSecurityException
{
// 獲得KeyManagerFactory對象.
String alg=KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactory kmFact=KeyManagerFactory.getInstance(alg);
// 配置KeyManagerFactory對象使用的KeyStoree.我們通過一個文件加載
// KeyStore.
FileInputStream fis=new FileInputStream(keyStore);
KeyStore ks=KeyStore.getInstance("jks");
ks.load(fis, keyStorePassword.toCharArray());
fis.close();
// 使用獲得的KeyStore初始化KeyManagerFactory對象
kmFact.init(ks, keyStorePassword.toCharArray());
// 獲得KeyManagers對象
KeyManager[] kms=kmFact.getKeyManagers();
return kms;
}
首先的任務是獲得一個KeyManagerFactory對象,但是你必須知道應該使用哪種算法。JSSE中提供了一個缺省的KeyManagerFactory算法(程序員也可以通過指定ssl.KeyManagerFacotory.algorithm屬性指定缺省算法)。獲得KeyManagerFactory對象后就可以加載KeyStore文件了,程序中通過一個InputStream對象將信息從文件送入KeyStore對象中。在這個過程之前,KeyStore對象需要知道輸入流的格式(例子中我使用的是jks)和密鑰。當我們完成了KeyStore的加載后,我們就可以用它來初始化KeyManagerFactory對象了。通常在JSSE中,在KeyStore中的所有證書使用和KeyStore相同的密碼,但是通過創建KeyManagerFactory對象你可以突破這個限制。在初始化了KeyManagerFactory對象后,通常使用getKeyManager()方法來獲得KeyManager對象數組。程序員通過使用和getKeyMangers()方法類似的流程來初始化TrustManager數組,這里我就不再重復了。
實現一個KeyManager類
到目前為止,我們已經知道如何在程序中動態生成KeyStore和TrustStore了。最后一個例子將告訴你如何實現一個KeyManager類。
當客戶端和服務器端成功連接后,客戶器端會出現下面的信息:
1: New connection request
1: Request from CN=Alice, OU= Development, O=DCQ, L=ChongQing,
ST=ChongQing, C=CH
為了使程序使用指定的授權,我們需要實現X509KeyManager接口(X509KeyManager是JSSE中最常用的KeyManager)。X509KeyManager接口在SSL握手階段使用了幾個方法來獲得授權。下面是X509KeyManager接口獲得授權的過程:
3.JSSE將別名作為參數調用X509KeyManager接口的getCertificateChain()和getPrivateKey()方法,這樣就獲得了指定授權的相關信息。
在例子程序中,X509KeyManager接口的實現類是AliasForcingKeyManager。在該類中最重要的方法就是就是chooseClientAlias()方法。下面是該方法的源代碼:
Socket socket)
{
//對于每一種類型的授權,都需要調用一次getClientAliases()方法來驗
// 證別名是否有效.
boolean aliasFound=false;
for (int i=0; i< i++) !aliasFound; &&>
if (validAliases!=null) {
for (int j=0; j< !aliasFound; && j++)>
}
}
}
if (aliasFound) return alias;
else return null;
}
我們可以看到在程序中,chooserClientAlias()方法實際上多次調用了getClientAliases()方法,每次都針對不同的授權類型。AliasForingKeyManager還實現了X509KeyManager接口的其他五個方法,在這里就不再一一贅述了。
throws IOException, GeneralSecurityException
{
// 調用父類中的方法獲得TrustManager和KeyManager
KeyManager[] kms=getKeyManagers();
TrustManager[]tms=getTrustManagers();
// 如果指定了別名,將KeyManagers包裝在AliasForcingKeyManager對象中.
if (alias!=null) {
for (int i=0; i< i++)>
if (kms instanceofX509KeyManager)
kms=new AliasForcingKeyManager((X509KeyManager)kms,alias);
}
}
// 利用TrustManagers和已經被包裝的KeyManagers創建一個SSLContext對象.
SSLContext context=SSLContext.getInstance("SSL");
context.init(kms, tms, null);
// 獲得SocketFactory對象.
SSLSocketFactory ssf=context.getSocketFactory();
return ssf;
}
我們可以使用同樣的方法來替換TrustManager對象,這樣我們就可以控制JSSE驗證授權的機制。具體的實現就留給讀者朋友去解決了。
小結
在這篇文章中,我們講述了使用JSSE的一些小技巧。讀完這篇文章后,我相信大家因該知道如何通過編程實現下面的任務:
· 突破在JSSE中KeySotre的密鑰的每個授權的密鑰必須相同的限制。
作者簡介:馮睿畢業于美國北伊利諾大學計算機和電氣工程系,獲工程碩士學位。曾就職于NewMonics公司,進行Java虛擬機部分包的設計和開發和Java底層的性能優化工作。目前負責一些政府和企業級GIS系統的設計和實現。


