Java安全通信概述

 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

 

發佈了28 篇原創文章 · 獲贊 4 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章