1.安全通信介紹
計算機安全通信過程中,常使用消息摘要和消息驗證碼來保證傳輸的數據未曾被第三方修改。
消息摘要是對原始數據按照一定算法進行計算得到的結果,它主要檢測原始數據是否被修改過。消息摘要與加密不同,加密是對原始數據進行變換,可以從變換後的數據中獲得原始數據,而消息摘要是從原始數據中獲得一部分信息,它比原始數據少得多,因此消息摘要可以看作是原始數據的指紋。
例:下面一段程序計算一段字符串的消息摘要
package com.messagedigest; import java.security.*; public class DigestPass { public static void main(String[] args) throws Exception{ String str="Hello,I sent to you 80 yuan."; MessageDigest md = MessageDigest.getInstance("MD5");//常用的有MD5,SHA算法等 md.update(str.getBytes("UTF-8"));//傳入原始字串 byte[] re = md.digest();//計算消息摘要放入byte數組中 //下面把消息摘要轉換爲字符串 String result = ""; for(int i=0;i<re.length;i++){ result += Integer.toHexString((0x000000ff&re[i])|0xffffff00).substring(6); } System.out.println(result); } }
當我們有時需要對一個文件加密時,以上方式不再適用。
又例:下面一段程序計算從輸入(出)流中計算消息摘要。
package com.messagedigest; import java.io.*; import java.security.*; public class DigestInput { public static void main(String[] args) throws Exception{ String fileName = "test.txt"; MessageDigest md = MessageDigest.getInstance("MD5"); FileInputStream fin = new FileInputStream(fileName); DigestInputStream din = new DigestInputStream(fin,md);//構造輸入流 //DigestOutputStream dout = new DigestOutputStream(fout,md); //使用輸入(出)流可以自己控制何時開始和關閉計算摘要 //也可以不控制,將全過程計算 //初始時是從開始即開始計算,如我們可以開始時關閉,然後從某一部分開始,如下: //din.on(false); int b; while((b=din.read())!=-1){ //做一些對文件的處理 //if(b=='$') din.on(true); //當遇到文件中的符號$時纔開始計算 } byte[] re = md.digest();//獲得消息摘要 //下面把消息摘要轉換爲字符串 String result = ""; for(int i=0;i<re.length;i++){ result += Integer.toHexString((0x000000ff&re[i])|0xffffff00).substring(6); } System.out.println(result); } }
當A和B通信時,A將數據傳給B時,同時也將數據的消息摘要傳給B,B收到後可以用該消息摘要驗證A傳的消息是否正確。這時會產生問題,即若傳遞過程中別人修改了數據時,同時也修改了消息摘要。B就無法確認數據是否正確。消息驗證碼可以解決這一問題。
使用消息驗證碼的前提是 A和B雙方有一個共同的密鑰,這樣A可以將數據計算出來的消息摘要加密後發給B,以防止消息摘要被改。由於使用了共同的密鑰,所以稱爲“驗證碼”。
例、下面的程序即可利用共同的密鑰來計算消息摘要的驗證碼
package com.mac; import java.io.*; import java.security.*; import javax.crypto.*; import javax.crypto.spec.*; public class MyMac { public static void main(String[] args) throws Exception{ //這是一個消息摘要串 String str="TestString"; //共同的密鑰編碼,這個可以通過其它算法計算出來 byte[] kb={11,105,-119,50,4,-105,16,38,-14,-111,21,-95,70,-15,76,-74, 67,-88,59,-71,55,-125,104,42}; //獲取共同的密鑰 SecretKeySpec k = new SecretKeySpec(kb,"HMACSHA1"); //獲取Mac對象 Mac m = Mac.getInstance("HmacMD5"); m.init(k); m.update(str.getBytes("UTF-8")); byte[] re = m.doFinal();//生成消息碼 //下面把消息碼轉換爲字符串 String result = ""; for(int i=0;i<re.length;i++){ result += Integer.toHexString((0x000000ff&re[i])|0xffffff00).substring(6); } System.out.println(result); } }
使用以上兩種技術可以保證數據沒有經過改變,但接收者還無法確定數據是否確實是某個人發來的。儘管消息碼可以確定數據是某個有同樣密鑰的人發來的,但這要求雙方具有共享的密鑰,若有一組用戶共享,我們就無法確定數據的來源了。
基於SSL的數字簽名可以解決這一問題。數字簽名利用非對稱加密技術,發送者使用私鑰加密數據產生的消息摘要(簽名),接收者使用發送者的公鑰解密消息摘要以驗證簽名是否是某個人的。由於私鑰只有加密者纔有,因此如果接收者用某個公鑰解密了某個消息摘要,就可以確定這段消息摘要必然是對應的私鑰持有者發來的。
使用數字簽名的前提是接收數據者能夠確信驗證簽名時(用發送者的私鑰加密消息摘要)所用的公鑰確實是某個人的 (因爲有可能有人假告公鑰)。數字證書可以解決這個問題。
數字證書含有兩部分數據:一部分是對應主體(單位或個人)的信息,另一部分是這個主體所對應的公鑰。即數字證書保存了主體和它的公鑰的一一對應關係。同樣,數字證書也有可能被假造,如何判定數字證書的內容的真實性呢?所以,有效的數字證書必須經過權威 CA的簽名,即權威CA驗證數字證書的內容的真實性,然後再在數字證書上使用自己的私鑰簽名(相當於在證書加章確認)。
這樣,當用戶收到這樣的數字證書後,會用相應的權威 CA的公鑰驗證該證書的簽名(因爲權威的CA的公鑰在操作系統中己經安裝)。根據非對稱加密的原理,如果該證書不是權威CA簽名的,將不能通過驗證,即該證書是不可靠的。
若通過驗證,即可證明此證書含的信息(發信人的公鑰和信息)是無誤的。於是可以信任該證書,便可以通過該證書內含的公鑰來確認數據確實是發送者發來的。
於是,雙方通信時, A把數據的消息摘要用自己的私鑰加密(即簽名),然後把自己的數字證書和數據及簽名後的消息摘要一起發送給B,B處查看A的數字證書,如果A的數字證書是經過權威CA驗證可靠的,便信任A,便可使用A的數字證書中附帶的A的公鑰解密消息摘要(這一過程同時確認了發送數據的人又可以解密消息摘要),然後通過解密後的消息摘要驗證數據是否正確無誤沒被修改。
2.SSL安全證書
SSL(安全套接層)是Netscape公司在1994年開發的,最初用於WEB瀏覽器,爲瀏覽器與服務器間的數據傳遞提供安全保障,提供了加密、來源認證和數據完整性的功能。現在SSL3.0得到了普遍的使用,它的改進版TLS(傳輸層安全)已經成爲互聯網標準。SSL本身和TCP套接字連接是很相似的,在協議棧中,SSL可以被簡單的看作是安全的TCP連接,但是某些TCP連接的特性它是不支持的,比如帶外數據(out-of-bound)。
在構建基於Socket的C/S程序時,通過添加對SSL的支持來保障數據安全和完整是不錯的方法。完善的Java爲我們提供了簡單的實現方法:JSSE(Java安全套接字擴展)。JSSE是一個純Java實現的SSL和TLS協議框架,抽象了SSL和TLS複雜的算法,使安全問題變得簡單。JSSE已經成爲J2SE1.4版本中的標準組件,支持SSL 3.0和TLS 1.0。我們將通過一個具體的例子演示JSSE的一些基本應用。例子中的服務器端將打開一個SSL Socket,只有持有指定證書的客戶端可以與它連接,所有的數據傳遞都是加密的。
構造一個SSLSocket是非常簡單的:
SSLServerSocketFactory factory=(SSLServerSocketFactory)SSLServerSocketFactory.getDefault(); SSLServerSocket server = (SSLServerSocket) factory.createServerSocket(portNumber); SSLSocket socket = (SSLSocket);
但是執行這樣的程序會產生一個異常,報告找不到可信任的證書。SSLSocket和普通的Socket是不一樣的,它需要一個證書來進行安全認證。
1)證書
生成一個CA證書,在命令行下執行:
keytool
–genkey –keystore SSLKey –keyalg rsa –alias SSL |
黑體部分是用戶可以自己指定的參數,第一個參數是要生成的證書的名字,第二個參數是證書的別名。rsa指明瞭我們使用的加密方法。
系統會要求輸入證書發放者的信息,逐項輸入即可。
系統生成的文件命將會和證書名相同。證書可以提交給權威CA認證組織審覈,如果通過審覈,組織會提供信任擔保,向客戶擔保你的連接是安全的。當然這不是必須的。在我們的例子中會把證書直接打包到客戶端程序中,保證客戶端是授權用戶,避免僞造客戶,所以不需要提交審覈。
2)服務器端
現在可以編寫服務器端的代碼,與普通的Socket代碼不同,我們需要在程序中導入證書,並使用該證書構造SSLSocket。需要的說明的是:
KeyStore
ks=KeyStore.getInstance( "JKS" ); |
訪問Java密鑰庫,JKS是keytool創建的Java密鑰庫,保存密鑰。
KeyManagerFactory
kmf=KeyManagerFactory.getInstance( "SunX509" ); |
創建用於管理JKS密鑰庫的X.509密鑰管理器。
SSLContext
sslContext=SSLContext.getInstance( "SSLv3" ); |
構造SSL環境,指定SSL版本爲3.0,也可以使用TLSv1,但是SSLv3更加常用。
sslContext.init(kmf.getKeyManagers(), null , null ); |
初始化SSL環境。第二個參數是告訴JSSE使用的可信任證書的來源,設置爲null是從javax.net.ssl.trustStore中獲得證書。第三個參數是JSSE生成的隨機數,這個參數將影響系統的安全性,設置爲null是個好選擇,可以保證JSSE的安全性。
完整代碼如下:
/* *SSL Socket的服務器端 *@Author Bromon */ package org.ec107.ssl; import java.net.*; import javax.net.ssl.*; import java.io.*; import java.security.*; public class SSLServer { static int port=8266; //系統將要監聽的端口號,82.6.6是偶以前女朋友的生日^_^ static SSLServerSocket server; /* *構造函數 */ public SSLServer() { } /* *@param port 監聽的端口號 *@return 返回一個SSLServerSocket對象 */ private static SSLServerSocket getServerSocket(int thePort) { SSLServerSocket s=null; try { String key="SSLKey"; //要使用的證書名 char keyStorePass[]="12345678".toCharArray(); //證書密碼 char keyPassword[]="12345678".toCharArray(); //證書別稱所使用的主要密碼 KeyStore ks=KeyStore.getInstance("JKS"); //創建JKS密鑰庫 ks.load(new FileInputStream(key),keyStorePass); //創建管理JKS密鑰庫的X.509密鑰管理器 KeyManagerFactory kmf=KeyManagerFactory.getInstance("SunX509"); kmf.init(ks,keyPassword); SSLContext sslContext=SSLContext.getInstance("SSLv3"); sslContext.init(kmf.getKeyManagers(),null,null); //根據上面配置的SSL上下文來產生SSLServerSocketFactory,與通常的產生方法不同 SSLServerSocketFactory factory=sslContext.getServerSocketFactory(); s=(SSLServerSocket)factory.createServerSocket(thePort); }catch(Exception e) { System.out.println(e); } return(s); } public static void main(String args[]) { try { server=getServerSocket(port); System.out.println("在”+port+”端口等待連接..."); while(true) { SSLSocket socket=(SSLSocket)server.accept(); //將得到的socket交給CreateThread對象處理,主線程繼續監聽 new CreateThread(socket); } }catch(Exception e) { System.out.println("main方法錯誤80:"+e); } } } /* *內部類,獲得主線程的socket連接,生成子線程來處理 */ class CreateThread extends Thread { static BufferedReader in; static PrintWriter out; static Socket s; /* *構造函數,獲得socket連接,初始化in和out對象 */ public CreateThread(Socket socket) { try { s=socket; in=new BufferedReader(new InputStreamReader(s.getInputStream(),"gb2312")); out=new PrintWriter(s.getOutputStream(),true); start(); //開新線程執行run方法 }catch(Exception e) { System.out.println(e); } } /* *線程方法,處理socket傳遞過來的數據 */ public void run() { try { String msg=in.readLine(); System.out.println(msg); s.close(); }catch(Exception e) { System.out.println(e); } } }
將我們剛纔生成的證書放到程序所在的目錄下,上面的代碼就可以在編譯之後執行:
java
org.ec107.ssl.SSLServer |
在8266端口等待連接…
3) 客戶端
客戶端的代碼相對簡單,我們可以不在程序中指定SSL環境,而是在執行客戶端程序時指定。需要注意的是客戶端並沒有導入證書,而是採用了默認的工廠方法構造SSLSocket:
SSLSocketFactory
factory=(SSLSocketFactory)SSLSocketFactory.getDefault(); |
構造默認的工廠方法
Socket
s=factory.createSocket( "localhost" ,port); |
打開一個SSLSocket連接
/* *SSL Socket 的客戶端 *@Author Bromon */ import java.net.*; import javax.net.ssl.*; import javax.net.*; import java.io.*; public class SSLClient { static int port=8266; public static void main(String args[]) { try { SSLSocketFactory factory=(SSLSocketFactory)SSLSocketFactory.getDefault(); Socket s=factory.createSocket("localhost",port); PrintWriter out=new PrintWriter(s.getOutputStream(),true); out.println("安全的說你好"); out.close(); s.close(); }catch(Exception e) { System.out.println(e); } } }
把服務器產生的證書(SSLKey)拷貝到程序所在的目錄,執行這個程序的時候需要向javax.net.ssl.trustStore環境變量傳入證書名:
java
–Djavax.net.ssl.trustStore=SSLKey org.ec107.ssl.SSLClient |
可以在服務器的控制檯看到客戶端發送過來的數據。
執行客戶端可以有另一種方法,把證書拷貝到java home/lib/security目錄下,名字改爲jssecacerts,然後可以直接執行客戶端:
java
org.ec107.ssl.SSLClient |
程序會自動的到上述目錄下去尋找jssecacerts文件作爲默認的證書。需要注意的是這裏的java home並不是我們在安裝J2SE時指定的那個JAVA_HOME。可以執行一個程序來得到java home的位置:
public class GetJavaHome { public static void main(String args[]) { System.out.println(System.getProperty(“java.home”)); } }
一般情況下(windows 2K)hava home的位置是在C:Program FilesJavaj2re1.4.0_02,相對的,證書就應該拷貝到C:Program FilesJavaj2re1.4.0_02libsecurity下,如果安裝了自帶JDK的Java IDE,比如JBuilder,情況可能會有不同。
如果程序客戶在不持有證書的情況下直接進行連接,服務器端會產生運行時異常,不允許進行連接。
運行環境:windows 2K server,j2sdk1.4.1