天天看點

用SSL建構安全的Socket

##說明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認證組織稽核,如果通過稽核,組織會提供信任擔保,向客戶擔保你的連接配接是安全的。當然這不是必須的。在我們的例子中會把證書直接打包到用戶端程式中,保證用戶端是授權使用者,避免僞造客戶,是以不需要送出稽核。

二、 伺服器端

現在可以編寫伺服器端的代碼,與普通的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的安全性。

import java.net.*;
    import javax.net.ssl.*;
    import java.io.*;
    import java.security.*;

    public class SSLServer{
    static int port=;  
    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端口等待連接配接…
           

三、 用戶端

用戶端的代碼相對簡單,我們可以不在程式中指定SSL環境,而是在執行用戶端程式時指定。需要注意的是用戶端并沒有導入證書,而是采用了預設的工廠方法構造SSLSocket:

● SSLSocketFactory factory=(SSLSocketFactory)SSLSocketFactory.getDefault();

    構造預設的工廠方法

    ●Socket s=factory.createSocket("localhost",port);

    打開一個SSLSocket連接配接

     /*
    *SSL Socket 的用戶端
    *@Author Bromon
    */

    package org.ec107.ssl;

    import java.net.*;
    import javax.net.ssl.*;
    import javax.net.*;
    import java.io.*;

    public class SSLClient
    {
    static int port=;
    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

SSL雙向認證java實作

http://www.blogjava.net/stone2083/archive/2008/07/10/169015.html

本文通過模拟場景,介紹SSL雙向認證的java實作

預設的情況下,我認為讀者已經對SSL原理有一定的了解,是以文章中對SSL的原理,不做詳細的介紹。

如果有這個需要,那麼通過GOOGLE,可以搜尋到很多這樣的文章。

模拟場景:

Server端和Client端通信,需要進行授權和身份的驗證,即Client隻能接受Server的消息,Server隻能接受Client的消息。

實作技術:

JSSE(Java Security Socket Extension)

是Sun為了解決在Internet上的安全通訊而推出的解決方案。它實作了SSL和TSL(傳輸層安全)協定。在JSSE中包含了資料加密,伺服器驗證,消息完整性和用戶端驗證等技術。通過使用JSSE,開發人員可以在客戶機和伺服器之間通過TCP/IP協定安全地傳輸資料

為了實作消息認證。

Server需要:

1)KeyStore: 其中儲存服務端的私鑰

2)Trust KeyStore:其中儲存用戶端的授權證書

同樣,Client需要:

1)KeyStore:其中儲存用戶端的私鑰

2)Trust KeyStore:其中儲存服務端的授權證書

我們可以使用Java自帶的keytool指令,去生成這樣資訊檔案

1)生成服務端私鑰,并且導入到服務端KeyStore檔案中

keytool -genkey -alias serverkey -keystore kserver.keystore

過程中,分别需要填寫,根據需求自己設定就行

keystore密碼:123456

名字和姓氏:stone

組織機關名稱:eulic

組織名稱:eulic

城市或區域名稱:HZ

州或省份名稱:ZJ

國家代碼:CN

serverkey私鑰的密碼,不填寫和keystore的密碼一緻:123456

就可以生成kserver.keystore檔案

server.keystore是給服務端用的,其中儲存着自己的私鑰

2)根據私鑰,導出服務端證書

keytool -export -alias serverkey -keystore kserver.keystore -file server.crt

server.crt就是服務端的證書

3)将服務端證書,導入到用戶端的Trust KeyStore中

keytool -import -alias serverkey -file server.crt -keystore tclient.keystore

tclient.keystore是給用戶端用的,其中儲存着受信任的證書

4) 檢視服務端證書裡面的内容

keytool -list -v -keystore kserver.keystore

采用同樣的方法,生成用戶端的私鑰,用戶端的證書,并且導入到服務端的Trust KeyStore中

1)keytool -genkey -alias clientkey -keystore kclient.keystore

2)keytool -export -alias clientkey -keystore kclient.keystore -file client.crt

3)keytool -import -alias clientkey -file client.crt -keystore tserver.keystore

如此一來,生成的檔案分成兩組

服務端儲存:kserver.keystore tserver.keystore

用戶端儲存:kclient.keystore tclient.kyestore

接下來,就采用JSSE,分别生成SSLServerSocket,SSLSocket

服務端,生成SSLServerSocket代碼:

package testSSLSocket;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.TrustManagerFactory;


public class Server {
    private static final String SERVER_KEY_STORE_PASSWORD = "xxx"; storepass 也就是密鑰庫的密碼,進入檔案的密碼
    private static final String SERVER_TRUST_KEY_PASSWORD = "xxx";
    private static final int DEFAULT_PORT = ;

    public static void main(String[] args) {
        SSLServerSocket serverSocket = Server.getSSLServerSocket();
        while(true){
            try {
                Socket socket =serverSocket.accept();
                new CreateThread(socket);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static SSLServerSocket getSSLServerSocket() {
        SSLServerSocket serverSocket = null;
        try {
            SSLContext ctx= SSLContext.getInstance("SSL");
            KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
            TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
            KeyStore ks = KeyStore.getInstance("jks");
            KeyStore tks = KeyStore.getInstance("jks");
            ks.load(new FileInputStream("keytool/server/kserver.keystore"), SERVER_KEY_STORE_PASSWORD.toCharArray());
            tks.load(new FileInputStream("keytool/server/tserver.keystore"), SERVER_TRUST_KEY_PASSWORD.toCharArray());
            kmf.init(ks, SERVER_KEY_STORE_PASSWORD.toCharArray());//該函數的第二個參數是aliaspassword(密鑰的密碼)
            tmf.init(tks);
            ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
            serverSocket = (SSLServerSocket)ctx.getServerSocketFactory().createServerSocket(DEFAULT_PORT);  
            serverSocket.setNeedClientAuth(true);
        } catch (KeyManagementException | UnrecoverableKeyException
                | NoSuchAlgorithmException | KeyStoreException
                | CertificateException | IOException e) {
            e.printStackTrace();
        }   
        return serverSocket;
    }
}


class CreateThread extends Thread{
    private BufferedReader in;
    private PrintWriter out;
    private Socket socket;

    public CreateThread(Socket socket){
        try {
            this.socket = socket;
            in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "gb2312"));
            out = new PrintWriter(socket.getOutputStream());
            start();
        } catch (IOException e) {
            e.printStackTrace();
        }   
    }

    @Override
    public void run() {
        try {
            String msg = in.readLine();
            System.out.println("msg = "+ msg);
            out.println("hello");
            out.flush();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}
           

用戶端,生成SSLSocket的代碼,大同小異

package testSSLSocket;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

public class Client1 {
    private static final String CLIENT_KEY_STORE_PASSWORD = "xxx"; storepass 也就是密鑰庫的密碼,進入檔案的密碼
    private static final String CLIENT_TRUST_KEY_PASSWORD = "xxx";
    private static final String DEFAULT_HOST = "localhost";
    private static final int DEFAULT_PORT = ;


    public static void main(String[] args) {
        try {
            Socket socket =  Client1.getSocket();
            socket.setSoTimeout();
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "gb2312"));
            PrintWriter out = new PrintWriter(socket.getOutputStream());
            out.println("hello1");
            out.flush();
            System.out.println(in.readLine());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static Socket getSocket() {
        Socket socket = null;
        try {
            SSLContext ctx= SSLContext.getInstance("SSL");
            KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
            TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
            KeyStore ks = KeyStore.getInstance("jks");
            KeyStore tks = KeyStore.getInstance("jks");
            ks.load(new FileInputStream("keytool/client1/kclient.keystore"), CLIENT_KEY_STORE_PASSWORD.toCharArray());
            tks.load(new FileInputStream("keytool/client1/tclient.keystore"), CLIENT_TRUST_KEY_PASSWORD.toCharArray());
            kmf.init(ks, CLIENT_KEY_STORE_PASSWORD.toCharArray());//該函數的第二個參數是aliaspassword(密鑰的密碼)
            tmf.init(tks);
            ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
            socket = ctx.getSocketFactory().createSocket(DEFAULT_HOST, DEFAULT_PORT);   
        } catch (KeyManagementException | UnrecoverableKeyException
                | NoSuchAlgorithmException | KeyStoreException
                | CertificateException | IOException e) {
            e.printStackTrace();
        }   
        return socket;
    }

}
           

如此,就完成了服務端和用戶端之間的基于身份認證的互動。

client采用kclient.keystore中的clientkey私鑰進行資料加密,發送給server

server采用tserver.keystore中的client.crt證書(包含了clientkey的公鑰)對資料解密,如果解密成功,證明消息來自client,進行邏輯處理

server采用kserver.keystore中的serverkey私鑰進行資料叫米,發送給client

client采用tclient.keystore中的server.crt證書(包含了serverkey的公鑰)對資料解密,如果解密成功,證明消息來自server,進行邏輯處理

如果過程中,解密失敗,那麼證明消息來源錯誤。不進行邏輯處理。這樣就完成了雙向的身份認證。

下面我附上簡單的SSLServer.java SSLClient.java,供大家示範用。

啟動服務端的時候,大家不妨采用telnet 127.0.0.1 7777連接配接,看看能不能實作消息傳遞。

ssl demo

備注:

demo是采用maven建構項目的

demo檔案的編碼是用utf8,為了避免中文亂碼,請把workspace設定成utf8編碼

java中,可以檢視證書:

寫個簡單的方法:

FileInputStream fis = new FileInputStream(“cert.cer”);

CertificateFactory cf=CertificateFactory.getInstance(“X509”);

X509Certificate c=(X509Certificate) cf.generateCertificate(fis);

System.out.println(c.getSubjectDN());

//可以檢視下X509Certificate的一些get方法.

其實keytool僅僅是jdk中的工具.

openssl是更常用的一個工具

可見:http://www.openssl.org/

繼續閱讀