java安全之SSL
SSL/TLS的工作原理
l 基本概念
ü SSL(Secure Socket Layer)是netscape公司設計的主要用於web的安全傳輸協議。這種協議在WEB上獲得了廣泛的應用。
ü IETF(www.ietf.org)將SSL作了標準化,即RFC2246,並將其稱為TLS(Transport Layer Security),從技術上講,TLS 1.0與SSL 3.0的差別非常微小。
ü 基本原理:先非對稱加密傳遞對稱加密所要用的鑰匙,然後雙方用該鑰匙對稱加密和解米往來的資料。
l 工作過程
ü 瀏覽器向伺服器發出請求,詢問對方支援的對稱加密演算法和非對稱加密演算法;伺服器迴應自己支援的演算法。
ü 瀏覽器選擇雙方都支援的加密演算法,並請求伺服器出示自己的證書;伺服器迴應自己的證書。
ü 瀏覽器隨機產生一個用於本次會話的對稱加密的鑰匙,並使用伺服器證書中附帶的公鑰對該鑰匙進行加密後傳遞給伺服器;伺服器為本次會話保持該對稱加密的鑰匙。第三方不知道伺服器的私鑰,即使截獲了資料也無法解密。非對稱加密讓任何瀏覽器都可以與伺服器進行加密會話。
ü 瀏覽器使用對稱加密的鑰匙對請求訊息加密後傳送給伺服器,伺服器使用該對稱加密的鑰匙進行解密;伺服器使用對稱加密的鑰匙對響應訊息加密後傳送給瀏覽器,瀏覽器使用該對稱加密的鑰匙進行解密。第三方不知道對稱加密的鑰匙,即使截獲了資料也無法解密。對稱加密提高了加密速度。
l 要求
ü 伺服器端需安裝數字證書,使用者可能需要確認證書。
ü 會話過程中的加密與解密過程由瀏覽器與伺服器自動完成,對使用者完全透明。
SSL 和 HTTPS 不僅可以加密通訊,而且可以用於伺服器和客戶身份的驗證。
在jdk幫助文件的jsse部分,可以看到SSL工作原理介紹的內容。
使用keytool建立證書時,剛開始設定的使用者名稱一定要是伺服器的域名,否則,瀏覽器進行訪問時也將提示證書有問題:
1.一種情況是發證機關是否值得信賴的,好比你拿出來的駕照是不是交管局這樣的國家法定機構頒發的,還是你們村的村支書自己蓋的章。
2.還有一種就是證書機構確實是交管局頒發的,但並不是發給你的,而是發給你老婆的,你出示你老婆的駕照時,人家交警會問,這是我們發的證,但是是發給你的嗎?
講課時,剛開始建立的證書的使用者故意不給localhost,實驗失敗後,再將老的證書的名稱修改為其他名稱,接著再建立一個給localhost的新證書,並且證書別名用老證書的名稱,這樣,就不用修改伺服器端的配置,但要重新啟動伺服器才能生效。
SSL程式設計——預設引數方式
l 伺服器端程式
• 呼叫getDefault()靜態方法得到SSLServerSocketFactory例項物件
• 需要通過javax.net.ssl.keyStore和javax.net.ssl.keyStorePassword系統屬性指定keystore的位置與密碼,否則,KeyManager管理一個空的keystore,這可以通過在jsse文件中搜索getDefault關鍵字獲知。
• 呼叫SSLServerSocketFactory物件的createServerSocket()方法得到ServerSocket。
• 迴圈呼叫ServerSocket.accept()等待外部連線,並啟動新執行緒與每個連線的客戶端進行對話。
• 開啟瀏覽器進行訪問測試,必須在keystore中存入與主機名相一致的keyEntry,且將該keyEntry的證書安裝到瀏覽器中。
l 客戶端程式設計:
• 呼叫getDefault()靜態方法得到SSLSocketFactory例項物件
• 需要通過javax.net.ssl.trustStore系統屬性指定truststore的位置,由於不需要讀取私鑰資訊,所以不用設定keystore的密碼。
• 如果沒有設定javax.net.ssl.trustStore系統屬性,則查詢<java-home>/lib/security/jssecacerts,沒找到則接著查詢<java-home>/lib/security/cacerts。
• 如果上面的預設的truststore沒有找到,則TrustManager管理一個空的trueststore,這可以通過在jsse文件中搜索getDefault關鍵字獲知。
• 呼叫SSLSocketFactory物件的createSocket()方法連線伺服器,連線成功後與伺服器進行對話。
• 執行客戶端程式進行測試訪問,如果伺服器出示的證書不是由客戶端已經信任的CA簽名的,則必須在truststore中匯入伺服器端的證書。
在這種採用預設引數的方式下,伺服器端的keystore中只能儲存一個keyEntry,否則,伺服器程式就面臨不知道選用哪個keyEntry的問題了。
客戶端匯入證書時執行的命令為:C:\Java\jdk1.6.0_21\bin>keytool -importcert -keystoreC:\Java\jdk1.6.0_21\jre\lib\security\cacerts -file zxx1.cer
baidu搜尋SSLServerSocket,即有了參考程式碼。
伺服器端程式碼:
publicvoid init1()throws Exception{
Stringuser_home = System.getProperty("user.home");
System.out.println(user_home);
System.setProperty("javax.net.ssl.keyStore",user_home+ "/.keystore");
System.setProperty("javax.net.ssl.keyStorePassword","123456");
ServerSocketFactoryfactory = SSLServerSocketFactory.getDefault();
ServerSocketss = factory.createServerSocket(443);
while(true){
Sockets = ss.accept();
newThread(new Worker(s)).start();
}
}
privateclass Worker implements Runnable{
Sockets = null;
publicWorker(Socket s){
this.s= s;
}
publicvoid run() {
try {
bytebuf[] = new byte[10240];
intlen = s.getInputStream().read(buf);
if(len>0){
System.out.println(newString(buf,0,len));
s.getOutputStream().write("200ok\r\n".getBytes());
s.getOutputStream().write("Content-length:6\r\n\r\n".getBytes());
s.getOutputStream().write("1234567".getBytes());
s.getOutputStream().close();
s.getInputStream().close();
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
客戶端程式碼:關鍵的一點就是要匯入伺服器端的證書到自己的trustkeystore中。
privatestatic void work1() throws Exception{
SSLSocketFactoryfactory = (SSLSocketFactory)SSLSocketFactory.getDefault();
Socketsocket = factory.createSocket("localhost",443);
InputStreamips = socket.getInputStream() ;
OutputStreamops = socket.getOutputStream();
ops.write("GET/ x".getBytes());
byte[]buf = new byte[1024];
intlen = 0;
while((len=ips.read(buf))!=-1){
System.out.println(newString(buf,0,len));
}
ips.close();
ops.close();
}
總結:建立伺服器端Socket時,應該指定自己的私鑰和證書在哪和是什麼;建立客戶端Socket時,它要驗證伺服器端出示的證書是否是可信賴的,所以,對於信賴的證書應該匯入到jre/lib/security/cacerts檔案中。伺服器端的證書由keyManagerFactory類管理,客戶端的證書由TrustManagerFactory管理。
在JSSE中,即使客戶端程式沒有直接信任伺服器所使用的證書,但伺服器的證書是客戶所信任的其他證書籤發的,客戶端程式照樣可以正常執行。
SSL程式設計——定製SSLContext
l 伺服器端程式
• 呼叫getInstance()靜態方法得到SSLContext例項物件
• 呼叫SSLContext物件的init()方法進行初始化
• 呼叫init()方法時必須傳入KeyManager陣列, KeyManager陣列通過KeyManagerFactory物件從keystore中去讀取出來。
• 呼叫SSLContext物件的getServerSocketFactory()方法得到SSLServerSocketFactory例項物件,接著按照預設引數的例子編寫後續程式碼。
l 客戶端程式設計:
• 呼叫getInstance()靜態方法得到SSLContext例項物件
• 呼叫SSLContext物件的init()方法進行初始化
• 呼叫init()方法時必須傳入TrustManager陣列, TrustManager陣列通過TrustManagerFactory物件從truststore中去讀取出來。
• 呼叫SSLContext物件的getSocketFactory()方法得到SSLSocketFactory例項物件,接著按照預設引數的例子編寫後續程式碼。
l 擴充套件:讓伺服器端程式從keystore裡的多個KeyEntry中選擇一個:
• 編寫一個實現了X509KeyManager介面的自定義KeyManager類,這個自定義KeyManager類的chooseServerAlias方法返回要使用的keyEntry的別名, 而自定義KeyManager類中的其他方法則轉發給原來通過KeyManagerFactory獲得的KeyMananger物件。
• 實驗方式:在keystore中建立兩個keyEntry,先指定其中一個,然後再指定另外一個,這兩個當中必然有一個會導致客戶端出錯。
這種定製方式可以讓伺服器端從多個keyEntry中選擇一個keyEntry,
KeyManagerFactory.getInstance()方法接受的引數值可以在JSSE文件中搜索KeyManagerFactory看到,如何定製KeyManager的例子也可以在jsse文件中搜索,找到一個TrustManager定製的案例程式碼,可以推出如何定製KeyManager。
伺服器端程式碼:
publicvoid init2() throws Exception{
Stringuser_home = System.getProperty("user.home");
System.out.println(user_home);
char[]passphrase = "123456".toCharArray();
KeyStoreks = KeyStore.getInstance("JKS");
ks.load(newFileInputStream(user_home + "/.keystore"), passphrase);
KeyManagerFactorykmf =
KeyManagerFactory.getInstance("SunX509");
//如果keystore中只有一個keyEntry,則此處表示keyEntry的密碼可以與keystore的密碼不同。
kmf.init(ks,passphrase);
SSLContextsslContext = SSLContext.getInstance("TLS");
sslContext.init(
kmf.getKeyManagers(), null, null);
ServerSocketFactoryfactory = sslContext.getServerSocketFactory();
ServerSocketss = factory.createServerSocket(443);
while(true){
Sockets = ss.accept();
newThread(new Worker(s)).start();
}
}
客戶端程式碼:執行SSL客戶端程式時可以增加-Djavax.net.debug=all,在jsse文件搜尋debug可以看到更詳細的資訊。
privatestatic void work2() throws Exception{
char[]passphrase = "changeit".toCharArray();
KeyStoreks = KeyStore.getInstance("JKS");
ks.load(newFileInputStream("C:\\Java\\jdk1.6.0_21\\jre\\lib\\security\\cacerts"),passphrase);
TrustManagerFactorymanagerFactory = TrustManagerFactory.getInstance("PKIX");
managerFactory.init(ks);
SSLContextcontext = SSLContext.getInstance("TLS");
context.init(null,managerFactory.getTrustManagers(), null);
SSLSocketFactoryfactory = context.getSocketFactory();
Socketsocket = factory.createSocket("localhost",443);
InputStreamips = socket.getInputStream() ;
OutputStreamops = socket.getOutputStream();
ops.write("GET/ x".getBytes());
byte[]buf = new byte[1024];
intlen = 0;
while((len=ips.read(buf))!=-1){
System.out.println(newString(buf,0,len));
}
ips.close();
ops.close();
}
先在下面的第一個檔案中找到了SSLImplementation的資訊,然後根據這些SSLImplementation類找到相應的包,再找到下面的JSSEKeyManager.java檔案
jakarta-tomcat-5.5.9-src.zip\jakarta-tomcat-5.5.9-src\jakarta-tomcat-connectors\http11\src\java\org\apache\coyote\http11\Http11Protocol.java
jakarta-tomcat-5.5.9-src.zip\jakarta-tomcat-5.5.9-src\jakarta-tomcat-connectors\util\java\org\apache\tomcat\util\net\jsse\JSSEKeyManager.java
下面的程式演示瞭如何應用JSSEKeyManager:
jakarta-tomcat-5.5.9-src.zip\jakarta-tomcat-5.5.9-src\jakarta-tomcat-connectors\util\java\org\apache\tomcat\util\net\jsse\JSSE14SocketFactory.java
擴充套件後的伺服器端程式碼:
publicvoid init3() throws Exception{
Stringuser_home = System.getProperty("user.home");
System.out.println(user_home);
char[]passphrase = "123456".toCharArray();
KeyStoreks = KeyStore.getInstance("JKS");
ks.load(newFileInputStream(user_home + "/.keystore"), passphrase);
KeyManagerFactorykmf =
KeyManagerFactory.getInstance("SunX509");
kmf.init(ks,passphrase);
KeyManager[]kms = kmf.getKeyManagers();
for(inti=0;i<kms.length;i++){
kms[i]= new ItcastKeyManager((X509KeyManager)kms[i],"mykey");
}
SSLContextsslContext = SSLContext.getInstance("TLS");
sslContext.init(
kms, null, null);
ServerSocketFactoryfactory = sslContext.getServerSocketFactory();
ServerSocketss = factory.createServerSocket(443);
while(true){
Sockets = ss.accept();
newThread(new Worker(s)).start();
}
}
privateclass ItcastKeyManager implements X509KeyManager {
private X509KeyManager delegate;
private String serverKeyAlias;
publicItcastKeyManager(X509KeyManager delegate, String serverKeyAlias) {
super();
this.delegate= delegate;
this.serverKeyAlias= serverKeyAlias;
}
publicString chooseClientAlias(String[] keyType, Principal[] issuers,
Socketsocket) {
returndelegate.chooseClientAlias(keyType, issuers, socket);
}
@Override
publicString chooseServerAlias(String keyType, Principal[] issuers,
Socketsocket) {
returnserverKeyAlias;
}
@Override
publicX509Certificate[] getCertificateChain(String alias) {
returndelegate.getCertificateChain(alias);
}
@Override
publicString[] getClientAliases(String keyType, Principal[] issuers) {
returndelegate.getClientAliases(keyType, issuers);
}
@Override
publicPrivateKey getPrivateKey(String alias) {
returndelegate.getPrivateKey(alias);
}
@Override
publicString[] getServerAliases(String keyType, Principal[] issuers) {
returndelegate.getServerAliases(keyType, issuers);
}
}
Https協議程式設計
l 與TLS的區別
• 傳輸的資料內容應遵守Http協議格式
• 伺服器出示的證書所有者的名稱必須是URL地址中指定的主機名
l 基本步驟:
• 建立表示https協議路徑的URL例項物件,並獲得URLConnection物件。
• 呼叫URLConnection物件的setDoOutput()方法支援傳送實體內容。
• 獲得輸出和輸入流與伺服器進行互動。
l 擴充套件步驟:在程式中定製truststore的位置
• 全域性設定:使用javax.net.ssl.trustStore系統屬性指定truststore的位置。
• 針對單個連結的設定:使用SSLContext物件的init()方法載入TrustManagerFactory物件從truststore中讀取出來的TrustManager物件,然後呼叫SSLContext物件的getSocketFactory()方法得到SSLSocketFactory例項物件,最後再呼叫HttpsURLConnection物件的setSSLSocketFactory方法。
l 學員郵件問題:
• 註冊機構供了一個.p12檔案keystore.p12(裡面包含公鑰和金鑰),還有這個.p12檔案的密碼。自己通過通過keytool把p12檔案中的證書匯出來,再把它裝入jks的truststore中,命令為:
keytool-importcert -keystore keystore.p12 -storetype pkcs12 -file xxx.crt
keytool-importcert -keystore xxx.jks -file xxx.crt
• 載入keystore.p12的程式程式碼:
KeyStorekeyStore = KeyStore.getInstance("pkcs12");
錯誤教學法:伺服器端先不出示與主機名相匹配的證書,看到錯誤後後改為出示與主機名匹配的證書。
程式碼1:
privatestatic void https1() throws Exception{
URLurl = new URL("https://localhost/abcd");
URLConnectionconnection = url.openConnection();
connection.setDoOutput(true);
OutputStreamops = connection.getOutputStream();
ops.write("GET/ xsss".getBytes());
InputStreamips = connection.getInputStream() ;
byte[]buf = new byte[1024];
intlen = 0;
while((len=ips.read(buf))!=-1){
System.out.println(newString(buf,0,len));
}
ips.close();
ops.close();
}
程式碼2:
private static void https2() throwsException{
char[]passphrase = "changeit".toCharArray();
KeyStoreks = KeyStore.getInstance("JKS");
ks.load(newFileInputStream("C:\\Java\\jdk1.6.0_21\\jre\\lib\\security\\cacerts"),passphrase);
TrustManagerFactorymanagerFactory = TrustManagerFactory.getInstance("PKIX");
managerFactory.init(ks);
SSLContextcontext = SSLContext.getInstance("TLS");
context.init(null,managerFactory.getTrustManagers(), null);
SSLSocketFactoryfactory = context.getSocketFactory();
URLurl = new URL("https://localhost/abcd");
URLConnectionconnection = url.openConnection();
((HttpsURLConnection)connection).setSSLSocketFactory(factory);
connection.setDoOutput(true);
OutputStreamops = connection.getOutputStream();
ops.write("GET/ x".getBytes());
InputStreamips = connection.getInputStream() ;
byte[]buf = new byte[1024];
intlen = 0;
while((len=ips.read(buf))!=-1){
System.out.println(newString(buf,0,len));
}
ips.close();
ops.close();
}
Tomcat體系結構
Tomcat是由很多模組組合出來的一個大大程式,每個模組各司其責,每個模組是如何組合成一個大程式的呢?Server.xml檔案好比藥的配方。
一個Service元素代表一種服務,譬如,賣火車票是一個服務,而賣飛機票又是另一個服務,connector相當於某種服務下的一種售票方式,可以在火車站對外售票,也可以在售票點對外售票,engine用於處理買票的內部工作,不管通過哪種方式接收進來的賣票請求,內部賣票處理工作始終一樣,即都是用這個engine。這個機制的好處在於有非常好的擴充套件性,如果想增加網上買票,只要再加上一個網上賣票的Connector即可,engine還是原來的。
我餐館的例子更好,服務員接電話,廚師炒菜,有沒有廚師一個人接電話接受訂單,還帶炒菜的啊?這是單模組。
為Tomcat配置SSL功能的實驗步驟
解決問題的思路:
• tomcat要支援SSL這種連結方式,只需要擴充安裝一個支援SSL的Connector物件
• 客戶端訪問這個connector時,這個connector必須出示數字證書,這就需要先產生或獲取證書,然後讓聯結器使用此證書。
實驗步驟:
• 使用keytool建立或匯入Web伺服器所需要的證書。
• 修改server.xml檔案,為Tomat增加一個支援SSL功能的聯結器。取消其中對SSL聯結器的註釋,並根據安裝的數字證書資訊對一些引數進行調整即可。
• 編寫一個用於檢查訪問協議是否是https的jsp程式,如果不是,則將請求重定向為https協議。
直接開啟tomcat的https聯結器,根據報告的錯誤資訊就知道keystore檔案儲存在哪裡了?當然,這要求計算機上以前沒有使用keytool工具建立預設的證書儲存檔案,這個預設檔案為<當前登入使用者的主目錄>/.keystore,例如,C:\Documents and Settings\IBM\.keystore。
使用keytool為tomcat產生數字證書時,使用者的姓名部分必須填寫成tomcat伺服器的主機名。
在tomcat5.5中,沒有keyalias選項來指定用哪個證書,在tomcat 6.x開始,就多了一個選項來指定所用證書的名稱了
使用瀏覽器進行訪問時https://localhost:8443,千萬別忘了使用https和正確的埠號,一不小心就寫成了http和用錯了埠號。
根據瀏覽器的提示,安裝完證書後,關閉瀏覽器後,重新開啟瀏覽器進行訪問,就沒問題了。
從Tomcat文件中看到的:
Finally, using name-based virtual hosts ona secured connection can be problematic. This is a design limitation of the SSLprotocol itself. The SSL handshake, where the client browser accepts the servercertificate, must occur before the HTTP request is accessed. As a result, therequest information containing the virtual host name cannot be determined priorto authentication, and it is therefore not possible to assign multiple certificatesto a single IP address. If all virtual hosts on a single IP address need toauthenticate against the same certificate, the addition of multiple virtualhosts should not interfere with normal SSL operations on the server. Be aware,however, that most client browsers will compare the server's domain nameagainst the domain name listed in the certificate, if any (applicable primarilyto official, CA-signed certificates). If the domain names do not match, thesebrowsers will display a warning to the client user. In general, onlyaddress-based virtual hosts are commonly used with SSL in a productionenvironment.