[ASP.NET] 使用 SSL Socket 建立 Client 與 Server 連接

[C#][ASP.NET][SSL][Socket]當 Client 與 Server 需要建立一個溝通的管道時可以使用 Socket 的方式建立一個通道,但是使用單純的 Socket 連線通道可能會擔心傳輸資料的過程中可能被截取修改因而不夠安全,為了防止這種情況我們可以使用建立 SSL Socket 的方式來進行資料的傳輸,所以這篇文章就來說明一下該如何建立 SSL Socket 通道,說實在本人對於憑證這個東西不是很熟悉,雖然在MSDN中已經有範例指導該如何建立 SSL Socket 方法,但是還是在憑證的操作上卡了一下,所以也會將卡住的地方舉出說明以免各位也卡在那兒。

前言


  當 Client 與 Server 需要建立一個溝通的管道時可以使用 Socket 的方式建立一個通道,但是使用單純的 Socket 連線通道可能會擔心傳輸資料的過程中可能被截取修改因而不夠安全,為了防止這種情況我們可以使用建立 SSL Socket 的方式來進行資料的傳輸,所以這篇文章就來說明一下該如何建立 SSL Socket 通道,說實在本人對於憑證這個東西不是很熟悉,雖然在MSDN中已經有範例指導該如何建立 SSL Socket 方法,但是還是在憑證的操作上卡了一下,所以也會將卡住的地方舉出說明以免各位也卡在那兒。

 

  範例將使用 SslStream類別 來說明建立的方法,SslStream 傳輸方式提供了訊息機密性和完整性檢查,當使用 SslStream 時可以防止傳輸的資訊被有心人讀取或竄改,使用 SslStream 時需要配合 TcpClient 與 TcpListener 一起使用,當客戶端需要與服務器建立連線時需要提供X509憑證與服務器的X509憑證進行驗證,SSL 通訊協定協助為使用 SslStream 傳輸的訊息提供機密性和完整性檢查。 當在用戶端和伺服器之間進行敏感資訊通訊時,應當使用 SSL 連接,例如由SslStream 提供的連接。 使用 SslStream 可以協助防止任何人對在網路上傳輸的資訊進行讀取或竄改,在客戶端與服務器的憑證使用上差別在客戶端使用的憑證不需要包含私鑰(*.cer)而服務器則需要包含私鑰的憑證(*.pfx)。

 

建立與使用憑證


建立憑證

首先要使用SSL就需要先建立一個憑證,但該如何建立跟使用呢? 可以先參考 使用憑證 此篇文章。

對於憑證的建立我們可以使用 Makecert.exe 工具,如果有裝 Visual Studio 則可以透過以下方式為建立,「開始」→「所有程式」→「Microsoft Visual Studio 2010」→「Visual Studio Tools」→「Visual Studio 命令提示字元 (2010)」

開啟命令提示字元後輸入:makecert -r -pe -n "CN=SslSocket" -ss My -sky exchange

參數說明如下:

  • -r :建立自動簽名的憑證。
  • -pe :將產生的私密金鑰標記為可匯出。 如此可在憑證中加入私密金鑰。
  • -n :指定主體的憑證名稱,使用雙引號包覆名稱開頭必須加CN=。
  • -ss :指定主體的憑證存放區名稱,其儲存輸出憑證,My為憑證存放區的個人存放區。
  • -sky exchange :指定收受者的金鑰類型,必須是下列之一:signature(表示今要用於數位簽章),exchange(表示金鑰用於金鑰加密和金鑰交換),或一個代表提供程式類型的整數。

詳細的參數說明可以參考此文章 Makecert.exe (憑證建立工具)

 

匯出與匯入憑證

在上個步驟中我們已經建立好之後要使用的憑證,接下來就必須將建立好的憑證匯出供服務器使用以及匯入到客戶端的電腦中,而詳細的步驟如下。

匯出憑證

  1. 「開始」→「執行」→「輸入MMC」,開啟主控台
  2. 「檔案」→「新增或移除崁入式管理單元」,在「可用崁入式管理單元」清單中找到憑證後新增到「選取的崁入式管理單元」中

接下來就能夠看到剛剛建立的憑證在個人憑證內

將此憑證匯出成包含私鑰憑證與不包含私鑰憑證

匯入憑證

憑證產生完成後就需要將產生的憑證匯入,SslSocket.pfx 之後將提供給服務器使用,而 SslSocket.cer 將提供給客戶端使用,憑證匯入的方式如下。

 

服務器憑證

將 SslSocket.pfx 放置在專案底下提供程式取用。

客戶端憑證

將 SslSocket.cer 於客戶端電腦使用以下步驟匯入。

開啟「IE」→「工具」→「網際網路選項」→「內容」→「憑證」→「受信任的根憑證授權單位」→「匯入」

 

範例


Step 1

經過以上步驟後憑證的設定已經完成接下來就是要撰寫程式碼進行測試,首先建立一個 Windows Application 傳案當作 Server 使用。

拉一個表單視窗出來,如下

產生一個  SslSocket類別 加入以下程式碼


public sealed class SslSocket
{
    private static TcpListener listener;
    private static X509Certificate ServerCertificate = null;
    private static bool IsRun = true;

    private static string _Certificate = string.Empty;
    public static string Certificate
    {
        get { return _Certificate; }
        set { _Certificate = value; }
    }
    
    /// <summary>
    /// 執行服務器監聽
    /// </summary>
    public static void RunServer() 
    {

        // 建立X509憑證
        ServerCertificate = new X509Certificate(Certificate, "ssl");
        // 監聽任何IP Address來的訊息
        listener = new TcpListener(System.Net.IPAddress.Any, 17170);
        // 開啟監聽
        listener.Start();

        while (IsRun)
        {
            UpdateStatus(string.Format("{0}-等待客戶端連接", DateTime.Now.ToString("HH:mm:ss")));
            TcpClient client = listener.AcceptTcpClient();

            if (!IsRun)
            {
                listener.Stop();
                client.Close();
            }
            else
            {
                ProcessClient(client);
            }
        }
    }

    /// <summary>
    /// 停止服務器監聽
    /// </summary>
    public static void StopServer()
    {
        IsRun = false;
        UpdateStatus(string.Format("{0}-停止客戶端連接", DateTime.Now.ToString("HH:mm:ss")));
    }

    /// <summary>
    /// 接收客戶端訊息處理並回覆
    /// </summary>
    /// <param name="pClient"></param>
    private static void ProcessClient(TcpClient pClient)
    {
        SslStream sslStream = new SslStream(pClient.GetStream(), true);

        try
        {
            sslStream.AuthenticateAsServer(ServerCertificate, false, SslProtocols.Tls, true);
            sslStream.ReadTimeout = 5000;
            sslStream.WriteTimeout = 5000;
            UpdateStatus(string.Format("{0}-等待客戶端訊息", DateTime.Now.ToString("HH:mm:ss")));

            string messageData = ReadMessage(sslStream);

            UpdateStatus(string.Format("{0}-接收訊息內容: {1}", DateTime.Now.ToString("HH:mm:ss"), messageData));
            byte[] message = Encoding.UTF8.GetBytes(string.Format("服務器已接收此: {0} 訊息<EOF>", messageData));

            UpdateStatus(string.Format("{0}-回覆客戶端訊息", DateTime.Now.ToString("HH:mm:ss")));
            sslStream.Write(message);
        }
        catch (Exception)
        {
            sslStream.Close();
            pClient.Close();
            return;
        }
        finally
        {
            sslStream.Close();
            pClient.Close();
        }
    }

    /// <summary>
    /// 讀取訊息內容
    /// </summary>
    /// <param name="pSslStream"></param>
    /// <returns></returns>
    private static string ReadMessage(SslStream pSslStream)
    {
        byte[] buffer = new byte[2048];
        StringBuilder messageData = new StringBuilder();
        int bytes = -1;
        do
        {
            bytes = pSslStream.Read(buffer, 0, buffer.Length);

            Decoder decoder = Encoding.UTF8.GetDecoder();
            char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)];
            decoder.GetChars(buffer, 0, bytes, chars, 0);
            messageData.Append(chars);

            if (messageData.ToString().IndexOf("<EOF>") != -1)
            {
                break;
            }
        } while (bytes != 0);

        return messageData.ToString();
    }

    /// <summary>
    /// 更新主視窗ListBoxUI
    /// </summary>
    /// <param name="pMessage"></param>
    private static void UpdateStatus(string pMessage)
    {
        Form1.MainListBox.Invoke(new Action(() => Form1.MainListBox.Items.Add(pMessage)));
    }
}

  此類別中 RunServer 方法為啟動監聽需呼叫的方法,首先將透過 X509Certificate 建構函式 (String, String) 建立一個 X509 憑證存入金鑰容器,傳入參數為 (FileName, Password),之後建立 TcpListener 物件用來監聽來至於 TCP 客戶端的連結且在此需要指定本機 IP 及 Port ,而在 While 迴圈內則建立 TcpClient 物件取得客戶端來連接時的 NetworkStream 的資料流,在此 TcpListener 使用了 AcceptTcpClient (接受暫止連線要求) 方法,意思是此監聽將處於暫時靜止狀態,當客戶端有連結時才會回應,所以最好將 SslSocket 類別使用執行序執行以避免主線程阻塞。

  當客戶端已連接後會執行 ProcessClient 方法,此時將建立 SslStream 物件來接收客戶端傳送來的資料流並且進行服務器的憑證驗證,而後進行資料的讀取動作,其中<EOF>標籤為標註訊息的結尾判斷使用,最後將處理完的資料回入資料流中傳送至客戶端處理。

接著在表單程式中產生一個執行序去執行 RunServer 方法啟動監聽同時還需要指定服務器使用的憑證。


public partial class Form1 : Form
{
    public static ListBox MainListBox;

    public Form1()
    {
        InitializeComponent();

        MainListBox = this.lbxMsg;
    }

    private void btnStart_Click(object sender, EventArgs e)
    {
        Thread socket = new Thread(RunSocket);
        socket.IsBackground = true;
        socket.Start();
    }

    private void btnStop_Click(object sender, EventArgs e)
    {
        SslSocket.StopServer();
    }

    private void RunSocket()
    {
        SslSocket.Certificate = Application.StartupPath + @"\SslSocket.pfx";
        SslSocket.RunServer();
    }
}

 

Step 2

接下來建立一個客戶端用來連接服務器溝通,建立一個 Web 網站於方案中,將剛剛產生的 SslSocket.cer 憑證放置在網站底下,簡單拉一個測試畫面。

建立一個 SendToServer 類別,主要工作於將客戶端訊息傳送至服務器端,詳細代碼如下。


public class SendToServer
{
    public string HostAddress { get; set; }
    public int HostPort { get; set; }

    /// <summary>
    /// 建構子,傳入服務器IP及Port
    /// </summary>
    /// <param name="pHostAddress"></param>
    /// <param name="pHostPort"></param>
    public SendToServer(string pHostAddress, int pHostPort)
    {
        HostAddress = pHostAddress;
        HostPort = pHostPort;
    }
    
    /// <summary>
    /// 執行將訊息發送至服務器方法
    /// </summary>
    /// <param name="pMessage"></param>
    /// <returns></returns>
    public string SendMsgToServer(string pMessage)
    {
        TcpClient client = new TcpClient(HostAddress, HostPort);
        SslStream sslStream = new SslStream(client.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null);
        X509CertificateCollection certs = new X509CertificateCollection();
        X509Certificate cert = X509Certificate.CreateFromCertFile(HttpContext.Current.Server.MapPath(@"~/cer/SslSocket.cer"));
        certs.Add(cert);
        try
        {
            sslStream.AuthenticateAsClient("SslSocket", certs, System.Security.Authentication.SslProtocols.Tls, true);
        }
        catch (Exception ex)
        {
            client.Close();
            return ex.Message;
        }
        byte[] messsage = Encoding.UTF8.GetBytes(string.Format("{0}<EOF>", pMessage));
        sslStream.Write(messsage);
        sslStream.Flush();
        string serverMessage = ReadMessage(sslStream);
        client.Close();
        return serverMessage;
    }

    /// <summary>
    /// 讀取訊息內容
    /// </summary>
    /// <param name="pSslStream"></param>
    /// <returns></returns>
    private string ReadMessage(SslStream pSslStream)
    {
        byte[] buffer = new byte[2048];
        StringBuilder messageData = new StringBuilder();
        int bytes = -1;
        do
        {
            bytes = pSslStream.Read(buffer, 0, buffer.Length);

            Decoder decoder = Encoding.UTF8.GetDecoder();
            char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)];
            decoder.GetChars(buffer, 0, bytes, chars, 0);
            messageData.Append(chars);

            if (messageData.ToString().IndexOf("<EOF>") != -1)
            {
                break;
            }
        } while (bytes != 0);

        return messageData.ToString();
    }

    /// <summary>
    /// 驗證服務器SSL憑證
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="certificate"></param>
    /// <param name="chain"></param>
    /// <param name="sslPolicyErrors"></param>
    /// <returns></returns>
    public static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
    {
        if (sslPolicyErrors == SslPolicyErrors.None)
            return true;
        return false;
    }
}

  在 SendMsgToServer 方法中,開始先建立 TcpClient 物件連接至指定的服務器 IP 及 Port ,之後建立 SslStream 物件並透過 AuthenticateAsClient 方法驗證與服務器的憑證,驗證成功後將訊息轉換成 byte[] 寫入資料流中傳送至服務器處理,服務器處理完成後將回寫資料至客戶端進行解析後顯示。

  最後在測試頁面程式碼中,建立一個 SendToServer 物件並且指定其 IP 及 Port 後呼叫 SendMsgToServer 方法即可。


public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {

    }
    protected void btnSend_Click(object sender, EventArgs e)
    {
        SendToServer send = new SendToServer("127.0.0.1", 17170);
        lblResult.Text = send.SendMsgToServer(txtMessage.Text.Trim());
    }
}

 

測試結果

以上為參考MSDN範例而產生的一個簡單的使用說明。

 

範例程式碼


TSslSocket.rar

參考資料


SslStream 類別

X509Certificate 類別

TcpListener 類別

TcpClient 類別

NetworkStream 類別

 

 


以上文章敘述如有錯誤及觀念不正確,請不吝嗇指教
如有侵權內容也請您與我反應~謝謝您 :)