SslStream,禁用会话缓存

MSDN文档说

如果可能,框架会在创建SSL会话时对其进行缓存,并尝试将缓存会话重新用于新请求。 尝试重用SSL会话时,Framework使用ClientCertificates的第一个元素(如果有),或者如果ClientCertificates为空,则尝试重用匿名会话。

如何禁用此缓存?

目前我遇到了重新连接到服务器的问题(即,第一个连接工作正常,但尝试重新连接服务器会中断会话)。 重新启动应用程序有帮助(但当然仅适用于第一次连接尝试)。 我假设问题根是缓存。

我已经使用嗅探器检查了数据包,区别仅在于客户端Hello消息的单个位置:

第一次连接到服务器(成功):

截图

第二次连接尝试(没有程序重启,失败):

截图

区别似乎只是会话标识符。

PS我想避免使用第三方SSL客户端。 有合理的解决方案吗?

这是来自ru.stackoverflow的这个问题的翻译

缓存在SecureChannel内部处理 – 内部类包装SSPI并由SslStream使用。 我没有看到任何可用于禁用客户端连接的会话缓存的内容。

您可以使用reflection清除连接之间的缓存:

var sslAssembly = Assembly.GetAssembly(typeof(SslStream)); var sslSessionCacheClass = sslAssembly.GetType("System.Net.Security.SslSessionsCache"); var cachedCredsInfo = sslSessionCacheClass.GetField("s_CachedCreds", BindingFlags.NonPublic | BindingFlags.Static); var cachedCreds = (Hashtable)cachedCredsInfo.GetValue(null); cachedCreds.Clear(); 

但这是非常糟糕的做法。 考虑修复服务器端。

所以我有点不同地解决了这个问题。 我真的不喜欢反映这种私有静态方法来转储缓存的想法,因为你真的不知道你是通过这样做进入的; 你基本上是在绕过封装,这可能会导致无法预料的问题。 但实际上,我担心转储缓存的竞争条件,在发送请求之前,其他一些线程进来并建立一个新会话,然后我的第一个线程无意中劫持了该会话。 坏消息……无论如何,这就是我的所作所为。

我停下来思考是否有办法隔离这个过程,然后我的Android同事回忆起AppDomains的可用性。 我们都同意旋转一个应该允许Tcp / Ssl调用运行,与其他一切隔离。 这将允许缓存逻辑保持完整,而不会导致SSL会话之间的冲突。

基本上,我最初编写的SSL客户端是内部的一个单独的库。 然后在该库中,我有一个公共服务作为该客户的代理/调解员。 在应用程序层,我希望能够根据硬件类型在服务之间切换(在我的情况下是HSM服务),因此我将其包装到适配器中并与工厂一起使用的接口。 好的,那有什么关系呢? 好吧,它只是让干净地做这个AppDomain事情变得更容易,而不强迫公共服务的任何其他消费者(我所说的代理/调解员)的这种行为。 你不必遵循这个抽象,我只想在找到它们时分享好的抽象示例:)

现在,在适配器中,我基本上创建了域,而不是直接调用服务。 这是ctor:

 public VCRklServiceAdapter( string hostname, int port, IHsmLogger logger) { Ensure.IsNotNullOrEmpty(hostname, nameof(hostname)); Ensure.IsNotDefault(port, nameof(port), failureMessage: $"It does not appear that the port number was actually set (port: {port})"); Ensure.IsNotNull(logger, nameof(logger)); ClientId = Guid.NewGuid(); _logger = logger; _hostname = hostname; _port = port; // configure the domain _instanceDomain = AppDomain.CreateDomain( $"vcrypt_rkl_instance_{ClientId}", null, AppDomain.CurrentDomain.SetupInformation); // using the configured domain, grab a command instance from which we can // marshall in some data _rklServiceRuntime = (IRklServiceRuntime)_instanceDomain.CreateInstanceAndUnwrap( typeof(VCServiceRuntime).Assembly.FullName, typeof(VCServiceRuntime).FullName); } 

所有这一切都创建了一个命名域,我的实际服务将独立运行。 现在,我遇到的关于如何在域内实际执行的大多数文章都过度简化了它的工作方式。 这些示例通常涉及调用myDomain.DoCallback(() => ...); 这并没有错,但试图将数据输入和输出该域可能会成为问题,因为序列化可能会阻止你死在你的轨道上。 简单地说,在DoCallback()之外实例化的对象在从DoCallback内部调用时不是相同的对象,因为它们是在该域之外创建的(请参阅对象编组)。 因此,您可能会遇到各种序列化错误。 如果运行整个操作,输入和输出并且所有操作都可以从myDomain.DoCallback()内部发生,这不是问题,但如果您需要使用外部参数并将此AppDomain中的某些内容返回到原始域,则会出现问题。

我在这里遇到了一个不同的模式,这个模式对我有用,并解决了这个问题。 在我的示例ctor中查看_rklServiceRuntime = 。 这样做实际上是要求域实例化一个对象,让您充当该域的代理。 这将允许您对其中的一些对象进行编组。 这是我对IRklServiceRuntime实现:

 public interface IRklServiceRuntime { RklResponse Run(RklRequest request, string hostname, int port, Guid clientId, IHsmLogger logger); } public class VCServiceRuntime : MarshalByRefObject, IRklServiceRuntime { public RklResponse Run( RklRequest request, string hostname, int port, Guid clientId, IHsmLogger logger) { Ensure.IsNotNull(request, nameof(request)); Ensure.IsNotNullOrEmpty(hostname, nameof(hostname)); Ensure.IsNotDefault(port, nameof(port), failureMessage: $"It does not appear that the port number was actually set (port: {port})"); Ensure.IsNotNull(logger, nameof(logger)); // these are set here instead of passed in because they are not // serializable var clientCert = ApplicationValues.VCClientCertificate; var clientCerts = new X509Certificate2Collection(clientCert); using (var client = new VCServiceClient(hostname, port, clientCerts, clientId, logger)) { var response = client.RetrieveDeviceKeys(request); return response; } } } 

这inheritance自MarshallByRefObject,它允许它跨越AppDomain边界,并且有一个方法可以获取外部参数并从实例化它的域中执行逻辑。

现在回到服务适配器:现在所有服务适配器都要调用_rklServiceRuntime.Run(...)并输入必要的可序列化参数。 现在,我只需要根据需要创建尽可能多的服务适配器实例,它们都在自己的域中运行。 这对我有用,因为我的SSL调用很小而且简短,这些请求是在内部Web服务中进行的,其中像这样的实例化请求非常重要。 这是完整的适配器:

 public class VCRklServiceAdapter : IRklService { private readonly string _hostname; private readonly int _port; private readonly IHsmLogger _logger; private readonly AppDomain _instanceDomain; private readonly IRklServiceRuntime _rklServiceRuntime; public Guid ClientId { get; } public VCRklServiceAdapter( string hostname, int port, IHsmLogger logger) { Ensure.IsNotNullOrEmpty(hostname, nameof(hostname)); Ensure.IsNotDefault(port, nameof(port), failureMessage: $"It does not appear that the port number was actually set (port: {port})"); Ensure.IsNotNull(logger, nameof(logger)); ClientId = Guid.NewGuid(); _logger = logger; _hostname = hostname; _port = port; // configure the domain _instanceDomain = AppDomain.CreateDomain( $"vc_rkl_instance_{ClientId}", null, AppDomain.CurrentDomain.SetupInformation); // using the configured domain, grab a command instance from which we can // marshall in some data _rklServiceRuntime = (IRklServiceRuntime)_instanceDomain.CreateInstanceAndUnwrap( typeof(VCServiceRuntime).Assembly.FullName, typeof(VCServiceRuntime).FullName); } public RklResponse GetKeys(RklRequest rklRequest) { Ensure.IsNotNull(rklRequest, nameof(rklRequest)); var response = _rklServiceRuntime.Run( rklRequest, _hostname, _port, ClientId, _logger); return response; } ///  /// Releases unmanaged and - optionally - managed resources. ///  public void Dispose() { AppDomain.Unload(_instanceDomain); } } 

注意处理方法。 不要忘记卸载域名。 此服务实现了实现IDisposable的IRklService,因此当我使用它时,它与using语句一起using

这看起来有点人为,但它确实没有,现在逻辑将在它自己的域上独立运行,因此缓存逻辑保持完整但没有问题。 比干涉SSLSessionCache好多了!

请原谅任何命名不一致,因为我在撰写post后很快就清理了实际名称。我希望这有助于某人!