首頁 Java java教程 Java下SSL通訊原理的範例程式碼分享

Java下SSL通訊原理的範例程式碼分享

Mar 25, 2017 am 10:51 AM

有關SSL的原理和介紹在網上已經有不少,對於Java下使用keytool生成證書,配置SSL通信的教程也非常多。但如果我們無法親自動手做一個SSL Sever和SSL Client,可能就永遠無法深入理解Java環境下,SSL的通訊是如何實現的。對SSL中的各種概念的認識也可能僅限於可以使用的程度。本文透過建構一個簡單的SSL Server和SSL Client來說明Java環境下SSL的通訊原理。

首先我們先回顧一下常規的Java Socket程式設計。在Java下寫一個Socket伺服器和客戶端的範例還是比較簡單的。

服務端很簡單:偵聽8080端口,並把客戶端發送的字串返回。以下是服務端的程式碼:

package org.bluedash.tryssl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class Server extends Thread {
    private Socket socket;
    public Server(Socket socket) {
        this.socket = socket;
    }
    public void run() {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter writer = new PrintWriter(socket.getOutputStream());
            String data = reader.readLine();
            writer.println(data);
            writer.close();
            socket.close();
        } catch (IOException e) {

        }
    }
    public static void main(String[] args) throws Exception {
        while (true) {
            new Server((new ServerSocket(8080)).accept()).start();
        }
    }
}
登入後複製

客戶端也非常簡單:向服務端發起請求,發送一個」hello」字串,然後取得服務端的回傳。下面是客戶端的程式碼:

package org.bluedash.tryssl;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
    public static void main(String[] args) throws Exception {
        Socket s = new Socket("localhost", 8080);
        PrintWriter writer = new PrintWriter(s.getOutputStream());
        BufferedReader reader = new BufferedReader(new InputStreamReader(s.getInputStream()));
        writer.println("hello");
        writer.flush();
        System.out.println(reader.readLine());
        s.close();
    }
}
登入後複製

把服務端運行起來後,執行客戶端,我們將得到」hello」的回傳。

就是這樣一套簡單的網路通訊的程式碼,我們來把它改造成使用SSL通訊。在SSL通訊協定中,我們都知道首先服務端必須有一個數位證書,當客戶端連接到服務端時,會得到這個證書,然後客戶端會判斷這個證書是否是可信的,如果是,則交換頻道加密金鑰,進行通訊。如果不信任這個證書,則連線失敗。

因此,我們首先要為服務端產生一個數位憑證。 Java環境下,數位憑證是用keytool產生的,這些憑證被儲存在store的概念中,就是憑證倉庫。我們來呼叫keytool指令為服務端產生數位憑證和儲存它所使用的憑證倉庫:

keytool -genkey -v -alias bluedash-ssl-demo-server -keyalg RSA -keystore ./server_ks 
-dname "CN=localhost,OU=cn,O=cn,L=cn,ST=cn,C=cn" -storepass server -keypass 123123
登入後複製

這樣,我們就將服務端憑證bluedash-ssl-demo-server儲存在server_ksy這個store檔案當中。有關keytool的用法在本文中就不再多贅述。執行上面的指令得到以下結果:

Generating 1,024 bit RSA key pair and self-signed certificate (SHA1withRSA) with a validity of 90 days
        for: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
[Storing ./server_ks]
登入後複製

然後,改造我們的服務端程式碼,讓服務端使用這個證書,並提供SSL通訊:

package org.bluedash.tryssl;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.KeyStore;

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

public class SSLServer extends Thread {
    private Socket socket;

    public SSLServer(Socket socket) {
        this.socket = socket;
    }

    public void run() {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter writer = new PrintWriter(socket.getOutputStream());

            String data = reader.readLine();
            writer.println(data);
            writer.close();
            socket.close();
        } catch (IOException e) {

        }
    }

    private static String SERVER_KEY_STORE = "/Users/liweinan/projs/ssl/src/main/resources/META-INF/server_ks";
    private static String SERVER_KEY_STORE_PASSWORD = "123123";

    public static void main(String[] args) throws Exception {
        System.setProperty("javax.net.ssl.trustStore", SERVER_KEY_STORE);
        SSLContext context = SSLContext.getInstance("TLS");

        KeyStore ks = KeyStore.getInstance("jceks");
        ks.load(new FileInputStream(SERVER_KEY_STORE), null);
        KeyManagerFactory kf = KeyManagerFactory.getInstance("SunX509");
        kf.init(ks, SERVER_KEY_STORE_PASSWORD.toCharArray());
        context.init(kf.getKeyManagers(), null, null);
        ServerSocketFactory factory = context.getServerSocketFactory();
        ServerSocket _socket = factory.createServerSocket(8443);
        ((SSLServerSocket) _socket).setNeedClientAuth(false);

        while (true) {
            new SSLServer(_socket.accept()).start();
        }
    }
}
登入後複製

可以看到,服務端的Socket準備設定工作大大增加了,增加的程式碼的作用主要是將憑證匯入並進行使用。此外,所使用的Socket變成了SSLServerSocket,另外連接埠改到了8443(這個不是強制的,只是為了遵守習慣)。另外,最重要的一點,服務端憑證裡面的CN一定和服務端的網域統一,我們的憑證服務的網域是localhost,那麼我們的客戶端在連接服務端時一定也要用localhost來連接,否則根據SSL協定標準,網域名稱與憑證的CN不匹配,說明這個憑證是不安全的,通訊將無法正常運作。

有了服務端,我們原來的客戶端就不能使用了,必須要走SSL協定。由於服務端的憑證是我們自己產生的,沒有任何受信任機構的簽名,所以客戶端是無法驗證服務端憑證的有效性的,通訊必然會失敗。所以我們需要為客戶端建立一個保存所有信任憑證的倉庫,然後把服務端憑證導進這個倉庫。這樣,當客戶端連接服務端時,會發現服務端的憑證在自己的信任清單中,就可以正常通訊了。

因此現在我們要做的是產生一個客戶端的憑證倉庫,因為keytool不能只產生一個空白倉庫,所以和服務端一樣,我們還是會產生一個憑證加一個倉庫(客戶端憑證加倉庫) :

keytool -genkey -v -alias bluedash-ssl-demo-client -keyalg RSA -keystore ./client_ks 
-dname "CN=localhost,OU=cn,O=cn,L=cn,ST=cn,C=cn" -storepass client -keypass 456456
登入後複製

結果如下:

Generating 1,024 bit RSA key pair and self-signed certificate (SHA1withRSA) with a validity of 90 days
        for: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
[Storing ./client_ks]
登入後複製

接下來,我們要把服務端的憑證匯出來,並匯入到客戶端的倉庫。第一步是匯出服務端的憑證:

keytool -export -alias bluedash-ssl-demo-server -keystore ./server_ks -file server_key.cer
登入後複製

執行結果如下:

Enter keystore password:  server
Certificate stored in file <server_key.cer>
登入後複製

然後是把匯出的憑證匯入到客戶端憑證倉庫:

keytool -import -trustcacerts -alias bluedash-ssl-demo-server -file ./server_key.cer -keystore ./client_ks
登入後複製

結果如下:

Enter keystore password:  client
Owner: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
Issuer: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
Serial number: 4c57c7de
Valid from: Tue Aug 03 15:40:14 CST 2010 until: Mon Nov 01 15:40:14 CST 2010
Certificate fingerprints:
         MD5:  FC:D4:8B:36:3F:1B:30:EA:6D:63:55:4F:C7:68:3B:0C
         SHA1: E1:54:2F:7C:1A:50:F5:74:AA:63:1E:F9:CC:B1:1C:73:AA:34:8A:C4
         Signature algorithm name: SHA1withRSA
         Version: 3
Trust this certificate? [no]:  yes
Certificate was added to keystore
登入後複製

好,準備工作做完了,我們來撰寫客戶端的程式碼:

package org.bluedash.tryssl;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

public class SSLClient {

    private static String CLIENT_KEY_STORE = "/Users/liweinan/projs/ssl/src/main/resources/META-INF/client_ks";

    public static void main(String[] args) throws Exception {
        // Set the key store to use for validating the server cert.
        System.setProperty("javax.net.ssl.trustStore", CLIENT_KEY_STORE);

        System.setProperty("javax.net.debug", "ssl,handshake");

        SSLClient client = new SSLClient();
        Socket s = client.clientWithoutCert();

        PrintWriter writer = new PrintWriter(s.getOutputStream());
        BufferedReader reader = new BufferedReader(new InputStreamReader(s
                .getInputStream()));
        writer.println("hello");
        writer.flush();
        System.out.println(reader.readLine());
        s.close();
    }

    private Socket clientWithoutCert() throws Exception {
        SocketFactory sf = SSLSocketFactory.getDefault();
        Socket s = sf.createSocket("localhost", 8443);
        return s;
    }
}
登入後複製

可以看到,除了把一些類別變成SSL通訊類別以外,客戶端也多出了使用信任憑證倉庫的代碼。以上,我們便完成了SSL單向握手通訊。即:客戶端驗證服務端的證書,服務端不認證客戶端的證書。
以上便是Java環境下SSL單向握手的全過程。因為我們在客戶端設定了日誌輸出等級為DEBUG:

System.setProperty("javax.net.debug", "ssl,handshake");
登入後複製

因此我們可以看到SSL通訊的全過程,這些日誌可以幫助我們更具體地了解透過SSL協定建立網路連線時的全過程。
結合日誌,我們來看看SSL雙向認證的整個過程:

第一步: 用戶端發送ClientHello訊息,發起SSL連線請求,告訴伺服器自己支援的SSL選項(加密方式等)。

*** ClientHello, TLSv1
登入後複製

第二步: 伺服器回應請求,回覆ServerHello訊息,和客戶端確認SSL加密方式:

*** ServerHello, TLSv1
登入後複製

第三步: 服務端向客戶端發布自己的公鑰。

第四步: 客户端与服务端的协通沟通完毕,服务端发送ServerHelloDone消息:

*** ServerHelloDone
登入後複製

第五步: 客户端使用服务端给予的公钥,创建会话用密钥(SSL证书认证完成后,为了提高性能,所有的信息交互就可能会使用对称加密算法),并通过ClientKeyExchange消息发给服务器:

*** ClientKeyExchange, RSA PreMasterSecret, TLSv1
登入後複製

第六步: 客户端通知服务器改变加密算法,通过ChangeCipherSpec消息发给服务端:

main, WRITE: TLSv1 Change Cipher Spec, length = 1
登入後複製

第七步: 客户端发送Finished消息,告知服务器请检查加密算法的变更请求:

*** Finished
登入後複製
登入後複製

第八步:服务端确认算法变更,返回ChangeCipherSpec消息

main, READ: TLSv1 Change Cipher Spec, length = 1
登入後複製

第九步:服务端发送Finished消息,加密算法生效:

*** Finished
登入後複製
登入後複製

那么如何让服务端也认证客户端的身份,即双向握手呢?其实很简单,在服务端代码中,把这一行:

((SSLServerSocket) _socket).setNeedClientAuth(false);
登入後複製

改成:

((SSLServerSocket) _socket).setNeedClientAuth(true);
登入後複製

就可以了。但是,同样的道理,现在服务端并没有信任客户端的证书,因为客户端的证书也是自己生成的。所以,对于服务端,需要做同样的工作:把客户端的证书导出来,并导入到服务端的证书仓库:

keytool -export -alias bluedash-ssl-demo-client -keystore ./client_ks -file client_key.cer
Enter keystore password:  client
Certificate stored in file <client_key.cer>

keytool -import -trustcacerts -alias bluedash-ssl-demo-client -file ./client_key.cer -keystore ./server_ks
Enter keystore password:  server
Owner: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
Issuer: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
Serial number: 4c57c80b
Valid from: Tue Aug 03 15:40:59 CST 2010 until: Mon Nov 01 15:40:59 CST 2010
Certificate fingerprints:
         MD5:  DB:91:F4:1E:65:D1:81:F2:1E:A6:A3:55:3F:E8:12:79
         SHA1: BF:77:56:61:04:DD:95:FC:E5:84:48:5C:BE:60:AF:02:96:A2:E1:E2
         Signature algorithm name: SHA1withRSA
         Version: 3
Trust this certificate? [no]:  yes
Certificate was added to keystore
登入後複製

完成了证书的导入,还要在客户端需要加入一段代码,用于在连接时,客户端向服务端出示自己的证书:

package org.bluedash.tryssl;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.security.KeyStore;
import javax.net.SocketFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;

public class SSLClient {
    private static String CLIENT_KEY_STORE = "/Users/liweinan/projs/ssl/src/main/resources/META-INF/client_ks";
    private static String CLIENT_KEY_STORE_PASSWORD = "456456";

    public static void main(String[] args) throws Exception {
        // Set the key store to use for validating the server cert.
        System.setProperty("javax.net.ssl.trustStore", CLIENT_KEY_STORE);
        System.setProperty("javax.net.debug", "ssl,handshake");
        SSLClient client = new SSLClient();
        Socket s = client.clientWithCert();

        PrintWriter writer = new PrintWriter(s.getOutputStream());
        BufferedReader reader = new BufferedReader(new InputStreamReader(s.getInputStream()));
        writer.println("hello");
        writer.flush();
        System.out.println(reader.readLine());
        s.close();
    }

    private Socket clientWithoutCert() throws Exception {
        SocketFactory sf = SSLSocketFactory.getDefault();
        Socket s = sf.createSocket("localhost", 8443);
        return s;
    }

    private Socket clientWithCert() throws Exception {
        SSLContext context = SSLContext.getInstance("TLS");
        KeyStore ks = KeyStore.getInstance("jceks");

        ks.load(new FileInputStream(CLIENT_KEY_STORE), null);
        KeyManagerFactory kf = KeyManagerFactory.getInstance("SunX509");
        kf.init(ks, CLIENT_KEY_STORE_PASSWORD.toCharArray());
        context.init(kf.getKeyManagers(), null, null);

        SocketFactory factory = context.getSocketFactory();
        Socket s = factory.createSocket("localhost", 8443);
        return s;
    }
}
登入後複製

通过比对单向认证的日志输出,我们可以发现双向认证时,多出了服务端认证客户端证书的步骤:

*** CertificateRequest
Cert Types: RSA, DSS
Cert Authorities:
<CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn>
<CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn>
*** ServerHelloDone

*** CertificateVerify
main, WRITE: TLSv1 Handshake, length = 134
main, WRITE: TLSv1 Change Cipher Spec, length = 1
登入後複製

在 @*** ServerHelloDone@ 之前,服务端向客户端发起了需要证书的请求 @*** CertificateRequest@ 。
在客户端向服务端发出 @Change Cipher Spec@ 请求之前,多了一步客户端证书认证的过程 @*** CertificateVerify@ 。
客户端与服务端互相认证证书的情景,可参考下图:

以上是Java下SSL通訊原理的範例程式碼分享的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
2 週前 By 尊渡假赌尊渡假赌尊渡假赌
倉庫:如何復興隊友
4 週前 By 尊渡假赌尊渡假赌尊渡假赌
Hello Kitty Island冒險:如何獲得巨型種子
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

Java 中的平方根 Java 中的平方根 Aug 30, 2024 pm 04:26 PM

Java 中的平方根

Java 中的完美數 Java 中的完美數 Aug 30, 2024 pm 04:28 PM

Java 中的完美數

Java 中的隨機數產生器 Java 中的隨機數產生器 Aug 30, 2024 pm 04:27 PM

Java 中的隨機數產生器

Java中的Weka Java中的Weka Aug 30, 2024 pm 04:28 PM

Java中的Weka

Java 中的阿姆斯壯數 Java 中的阿姆斯壯數 Aug 30, 2024 pm 04:26 PM

Java 中的阿姆斯壯數

Java 中的史密斯數 Java 中的史密斯數 Aug 30, 2024 pm 04:28 PM

Java 中的史密斯數

Java Spring 面試題 Java Spring 面試題 Aug 30, 2024 pm 04:29 PM

Java Spring 面試題

突破或從Java 8流返回? 突破或從Java 8流返回? Feb 07, 2025 pm 12:09 PM

突破或從Java 8流返回?

See all articles