C#使用 SSL Socket 建立 Client 与 Server 连接
当 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 (凭证建立工具)
导出与汇入凭证
在上个步骤中我们已经建立好之后要使用的凭证,接下来就必须将建立好的凭证导出供服务器使用以及汇入到客户端的计算机中,而详细的步骤如下。
导出凭证
- 「开始」→「执行」→「输入MMC」,开启控制台
- 「档案」→「新增或移除崁入式管理单元」,在「可用崁入式管理单元」列表中找到凭证后新增到「选取的崁入式管理单元」中
接下来就能够看到刚刚建立的凭证在个人凭证内
将此凭证导出成包含私钥凭证与不包含私钥凭证
汇入凭证
凭证产生完成后就需要将产生的凭证汇入,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范例而产生的一个简单的使用说明。