根据请求在.net HttpWebRequest上设置SecurityProtocol(Ssl3或TLS)

Set the SecurityProtocol (Ssl3 or TLS) on the .net HttpWebRequest per request

我的应用程序(.net 3.5 sp1)使用httpwebrequest与不同的端点通信,有时通过https进行通信,其中每个宿主服务器可能有不同的安全协议要求,如tls或ssl3或两者之一。

通常情况下,服务器在使用TLS或SSL3的安全协议方面表现良好且愉快地协商/回退,但有些服务器没有,并且当.NET设置为TLS或SSL3(默认情况下,我认为)时,仅支持SSL3的服务器会导致.NET引发发送错误。

据我所知,.NET为ServicePointManager对象提供了一个属性securityProtocol,该属性可以设置为tls、ssl3或两者都设置。因此,理想情况下,当同时设置为IDEA时,客户机和服务器应该就使用什么进行协商,但正如前面所述,这似乎不起作用。

假设您可以设置servicePointManager.securityProtocol=ssl3,但是想要使用tl的端点呢?

我在ServicePointManager和SecurityProtocol中看到的问题是,它是静态的,因此也适用于整个应用领域。

所以对于这个问题……

如何使用具有不同安全协议的httpwebrequest,例如

1)URL 1设置为使用tls ssl3(协商)

2)URL 2设置为SSL3(仅限SSL3)


不幸的是,您似乎无法根据每个服务点定制此服务。我建议您在MS Connect网站上为此区域提交功能请求。

作为一个肮脏的解决方案,您可以尝试在新的AppDomain中执行需要不同安全协议的站点。静态实例是每个AppDomain的,因此应该为您提供所需的隔离。


我有同样的问题,并编写了代理类,它打开本地主机上的端口,并将所有通信转发到指定的主机:端口。

所以连接是这样的

[您的代码]---http--->[本地主机上的代理:端口]---https--->[网站]

事实上,它可以用来将任何协议包装成SSL/TLS,而不仅仅是HTTP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;

namespace System
{
    class sslProxy : IDisposable
    {
        readonly string host;
        readonly int port;
        readonly TcpListener listener;
        readonly SslProtocols sslProtocols;
        bool disposed;
        static readonly X509CertificateCollection sertCol = new X509CertificateCollection();
        public sslProxy(string url, SslProtocols protocols)
        {
            var uri = new Uri(url);
            host = uri.Host;
            port = uri.Port;
            sslProtocols = protocols;
            listener = new TcpListener(IPAddress.Loopback, 0);
            listener.Start();
            listener.BeginAcceptTcpClient(onAcceptTcpClient, null);
            Proxy = new WebProxy("localhost", (listener.LocalEndpoint as IPEndPoint).Port);
        }
        public WebProxy Proxy
        {
            get;
            private set;
        }
        class stBuf
        {
            public TcpClient tcs;
            public TcpClient tcd;
            public Stream sts;
            public Stream std;
            public byte[] buf;
            public stBuf dup;
        }
        void onAcceptTcpClient(IAsyncResult ar)
        {
            if (disposed) return;
            var tcl = listener.EndAcceptTcpClient(ar);
            TcpClient tcr = null;
            try
            {
                listener.BeginAcceptTcpClient(onAcceptTcpClient, null);
                var nsl = tcl.GetStream();

                tcr = new TcpClient(host, port);
                Stream nsr = tcr.GetStream();
                if (sslProtocols != SslProtocols.None)
                {
                    var sss = new SslStream(nsr, true);
                    sss.AuthenticateAsClient(host, sertCol, sslProtocols, false);
                    nsr = sss;
                } // if

                var sts = new stBuf() { tcs = tcl, sts = nsl, tcd = tcr, std = nsr, buf = new byte[tcl.ReceiveBufferSize] };
                var std = new stBuf() { tcs = tcr, sts = nsr, tcd = tcl, std = nsl, buf = new byte[tcr.ReceiveBufferSize] };
                sts.dup = std;
                std.dup = sts;

                nsl.BeginRead(sts.buf, 0, sts.buf.Length, onReceive, sts);
                nsr.BeginRead(std.buf, 0, std.buf.Length, onReceive, std);
            } // try
            catch
            {
                tcl.Close();
                if (tcr != null) tcr.Close();
            } // catch
        }
        void close(stBuf st)
        {
            var dup = st.dup;
            if (dup != null)
            {
                dup.dup = st.dup = null;
                st.sts.Dispose();
                st.std.Dispose();
            } // if
        }
        void onReceive(IAsyncResult ar)
        {
            var st = ar.AsyncState as stBuf;
            try
            {
                if (!(st.dup != null && st.tcs.Connected && st.sts.CanRead && !disposed)) { close(st); return; };
                var n = st.sts.EndRead(ar);
                if (!(n > 0 && st.tcd.Connected && st.std.CanWrite)) { close(st); return; };
                st.std.Write(st.buf, 0, n);
                if (!(st.tcs.Connected && st.tcd.Connected && st.sts.CanRead && st.std.CanWrite)) { close(st); return; };
                st.sts.BeginRead(st.buf, 0, st.buf.Length, onReceive, st);
            } // try
            catch
            {
                close(st);
            } // catch
        }
        public void Dispose()
        {
            if (!disposed)
            {
                disposed = true;
                listener.Stop();
            } // if
        }
    }
}

使用实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// create proxy once and keep it
// note you have to mention :443 port (https default)
// ssl protocols to use (enum can use | or + to have many)
var p = new sslProxy("http://www.google.com:443", SslProtocols.Tls);
// using our connections
for (int i=0; i<5; i++)
{
    // url here goes without https just http
    var rq = HttpWebRequest.CreateHttp("http://www.google.com/") as HttpWebRequest;
    // specify that we are connecting via proxy
    rq.Proxy = p.Proxy;
    var rs = rq.GetResponse() as HttpWebResponse;
    var r = new StreamReader(rs.GetResponseStream()).ReadToEnd();
    rs.Dispose();
} // for
// just dispose proxy once done
p.Dispose();


在我们的一些供应商停止对SSL3的支持,而其他供应商只使用它之后,我们的系统中出现了许多问题,这些问题可以通过这个问题的功能来解决。但六年后,我们仍然没有内置机制来实现这一目标。我们的解决方法是明确定义支持所有场景的安全协议,如:

1
2
3
4
5
    System.Net.ServicePointManager.SecurityProtocol =
    System.Net.SecurityProtocolType.Ssl3
    | System.Net.SecurityProtocolType.Tls12
    | SecurityProtocolType.Tls11
    | SecurityProtocolType.Tls;


根据这个答案,SecurityProtocol设置实际上是按AppDomain设置的,因此,如果您确定要使其正常工作,可以为单独的设置创建单独的AppDomain,并跨范围整理查询。

不完全是一个"整洁"的解决方案,但可能只是让你所需要的不借助于第三方库成为可能。


有了NET 4.6,Windows(从Microsoft)可以使用httpclient和winhttphandler nuget包来设置sslprotocols参数。对于net core,可以使用httpclienthandler类。

1
2
3
4
5
6
7
8
9
10
11
12
using (var hc = new HttpClient(new WinHttpHandler() // should have it as a static member
{
    AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,
    SslProtocols = SslProtocols.Tls |
                   SslProtocols.Tls11 |
                   SslProtocols.Tls12
}))
{
    var r = hc.SendAsync(new HttpRequestMessage(HttpMethod.Get,"https://..."));
    r.Wait();
    Console.WriteLine(r.Result.StatusCode);
} // using


您可以通过此代码来实现这一点,以关闭所有基础连接并强制进行新的握手。

1
2
3
4
5
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
...
...
...
request.ServicePoint.CloseConnectionGroup(request.ConnectionGroupName);


我知道这个问题由来已久,但问题仍然存在于.NET 4.7.2中。在我的例子中,我有一个多线程应用程序正在与两个端点通信。一个端点只能与TLS 1.2一起工作,另一个端点只能与TLS 1.0一起工作(负责该端点的团队正在修复其端点,因此它也将支持TLS 1.2)。

为了解决这个问题,我将只与TLS 1.0一起工作的端点的服务调用移动到同一程序集中的一个单独类中,然后将该程序集加载到一个单独的AppDomain中。通过这样做,我可以使用这个全局变量:

1
System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;

仅对于对断开端点的调用,同时对TLS 1.2端点的调用(不需要将ServicePointManager.SecurityProtocol设置为任何特定值)将继续工作。这还确保了当好的端点升级到TLS 1.3时,我不需要重新发布我的应用程序。我的应用程序是多线程和高容量的,因此锁定或尝试/最终不是足够的解决方案。

下面是我用来将程序集加载到单独域中的代码。请注意,我从程序集的当前位置(aspnet tempfiles)加载程序集,这样它就不会在bin目录中锁定程序集并阻止将来的部署。

另外,请注意,代理类继承MarshalByRefObject,以便将其用作一个透明代理,该代理使用自己的AppDomain中的值保留System.NET.ServicePointManager。

对于.NET框架来说,这似乎是一个愚蠢的限制,我希望我们可以直接在Web请求上指定协议,而不是跳过一些限制,特别是在经过多年的努力之后。:(

此代码确实有效,希望它能帮助您!:)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private static AppDomain _proxyDomain = null;
private static Object _syncObject = new Object();

public void MakeACallToTls10Endpoint(string tls10Endpoint, string jsonRequest)
{
   if (_proxyDomain == null)
   {
      lock(_syncObject) // Only allow one thread to spin up the app domain.
      {
         if (_proxyDomain == null)
         {
            _proxyDomain = AppDomain.CreateDomain("CommunicationProxyDomain");
         }
      }
   }

   Type communicationProxyType = typeof(CommunicationProxy);
   string assemblyPath = communicationProxyType.Assembly.Location;

   // Always loading from the current assembly, sometimes this moves around in ASPNet Tempfiles causing type not found errors if you make it static.
   ObjectHandle objectHandle = _proxyDomain.CreateInstanceFrom(assemblyPath, communicationProxyType.FullName.Split(',')[0]);
   CommunicationProxy communicationProxy = (CommunicationProxy)objectHandle.Unwrap();

   return communicationProxy.ExecuteHttpPost(tls10Endpoint, jsonRequest);
}

然后,在一个单独的类中:

1
2
3
4
5
6
7
8
9
10
[Serializable]
public class CommunicationProxy : MarshalByRefObject
{
   public string ExecuteHttpPost(string tls10Endpoint, string jsonRequest)
   {
      ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;

      // << Bunch of code to do the request >>
   }
}

设置所有这些。在我的应用程序中,它适用于不同的安全协议。

1
2
3
4
5
System.Net.ServicePointManager.SecurityProtocol =
System.Net.SecurityProtocolType.Ssl3
| System.Net.SecurityProtocolType.Tls12
| SecurityProtocolType.Tls11
| SecurityProtocolType.Tls;


可以使用静态实用程序方法生成httpwebrequest"实用程序类"。在静态实用程序方法中,围绕设置servicepointmanager.securityprotocol和创建特定的httpwebrequest使用c_lock语句。lock语句防止来自同一AppDomain的其他线程同时执行同一代码,因此,在执行整个锁块(=critical section)之前,您刚刚设置的TLS协议不会更改。

但要知道,对于真正高性能的应用程序(非常高性能!)这种方法可能会对性能产生负面影响。