【问题标题】:SslStream, disable session cachingSslStream,禁用会话缓存
【发布时间】:2015-05-06 14:15:58
【问题描述】:

MSDN documentation

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

如何禁用此缓存?

目前,我在重新连接到服务器时遇到问题(即,第一个连接运行良好,但尝试重新连接服务器会中断会话)。重新启动应用程序会有所帮助(但当然仅适用于第一次连接尝试)。我认为问题根源在于缓存。

我已经用嗅探器检查了数据包,不同之处仅在于客户端问候消息的一个地方:

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

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

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

附:我想避免使用第 3 方 SSL 客户端。有没有合理的解决办法?

这是this question 来自 ru.stackoverflow 的翻译

【问题讨论】:

    标签: c# ssl-certificate


    【解决方案1】:

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

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

    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();
    

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

    【讨论】:

    • 或者修复 SslClient Microsoft。
    • @rufanov:嗯,你正在伸手去拿一把大枪。当然,反射必须起作用。
    • @Vlad,嗯...另一种选择是围绕 SSPI 编写自己的包装器,并且根本不使用 .NET SSL 类 XD。微软最好在 CLR 内部的任何地方都使用公共接口,而不是内部/密封的实现......但他们没有:(
    • 微软的tcp+tls架构可谓“根深蒂固”
    • 我同意,这并不是您投入生产的真正解决方案。微软应该尽快解决这个问题!
    【解决方案2】:

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

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

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

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

    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 中的某些内容返回到原始域,则会出现问题。

    我在 SO 上遇到了一种不同的模式,它对我有用并解决了这个问题。在我的示例 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;
            }
        }
    }
    

    这继承自 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;
        }
    
        /// <summary>
        /// Releases unmanaged and - optionally - managed resources.
        /// </summary>
        public void Dispose()
        {
            AppDomain.Unload(_instanceDomain);
        }
    }
    

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

    这似乎有点做作,但实际上并非如此,现在逻辑将在其自己的域上独立运行,因此缓存逻辑保持不变但没有问题。比干预 SSLSessionCache 要好得多!

    请原谅任何命名不一致的地方,因为我在写完帖子后正在快速清理实际名称。我希望这对某人有所帮助!

    【讨论】:

    • 我刚刚阅读了这篇文章,发现它可能会混淆 SslClient 的作用。在 VCServiceRuntime.Run() 内部,您将看到对 VCServiceClient 的调用。那是我对 SslClient 的包装器,它处理请求、证书、验证等的组装。那里没有什么了不起的。那里有几层间接来保持该服务的灵活性。 VCServiceRuntime 类是重要的部分,因为它是允许其内部的所有逻辑与应用程序域一起使用的包装器。
    • 听起来很痛苦但很有用。那是如何禁用会话缓存的?
    猜你喜欢
    • 1970-01-01
    • 2017-07-13
    • 2020-02-23
    • 1970-01-01
    • 2011-05-16
    • 1970-01-01
    • 2013-12-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多