6

我正在尝试使用 Katana 项目提供的 OpenID Connect 身份验证中间件。

实现中存在一个错误,在这些情况下会导致死锁:

  1. 在请求具有线程关联性的主机中运行(例如 IIS)。
  2. 尚未检索 OpenID Connect 元数据文档或缓存副本已过期。
  3. 应用程序调用SignOut身份验证方法。
  4. 应用程序中发生的操作会导致写入响应流。

死锁的发生是由于身份验证中间件处理来自主机的回调的方式,表明正在发送标头。问题的根源在于这种方法:

private static void OnSendingHeaderCallback(object state)
{
    AuthenticationHandler handler = (AuthenticationHandler)state;
    handler.ApplyResponseAsync().Wait();
}

来自Microsoft.Owin.Security.Infrastructure.AuthenticationHandler

仅当返回已经完成时,对的调用Task.Wait()才是安全的,而在 OpenID Connect 中间件的情况下它还没有完成。Task

中间件使用 的实例Microsoft.IdentityModel.Protocols.ConfigurationManager<T>来管理其配置的缓存副本。这是一个异步实现,使用SemaphoreSlim异步锁和 HTTP 文档检索器来获取配置。我怀疑这是死锁Wait()调用的触发器。

这是我怀疑是原因的方法:

public async Task<T> GetConfigurationAsync(CancellationToken cancel)
{
    DateTimeOffset now = DateTimeOffset.UtcNow;
    if (_currentConfiguration != null && _syncAfter > now)
    {
        return _currentConfiguration;
    }

    await _refreshLock.WaitAsync(cancel);
    try
    {
        Exception retrieveEx = null;
        if (_syncAfter <= now)
        {
            try
            {
                // Don't use the individual CT here, this is a shared operation that shouldn't be affected by an individual's cancellation.
                // The transport should have it's own timeouts, etc..

                _currentConfiguration = await _configRetriever.GetConfigurationAsync(_metadataAddress, _docRetriever, CancellationToken.None);
                Contract.Assert(_currentConfiguration != null);
                _lastRefresh = now;
                _syncAfter = DateTimeUtil.Add(now.UtcDateTime, _automaticRefreshInterval);
            }
            catch (Exception ex)
            {
                retrieveEx = ex;
                _syncAfter = DateTimeUtil.Add(now.UtcDateTime, _automaticRefreshInterval < _refreshInterval ? _automaticRefreshInterval : _refreshInterval);
            }
        }

        if (_currentConfiguration == null)
        {
            throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, ErrorMessages.IDX10803, _metadataAddress ?? "null"), retrieveEx);
        }

        // Stale metadata is better than no metadata
        return _currentConfiguration;
    }
    finally
    {
        _refreshLock.Release();
    }
}

我已经尝试添加.ConfigureAwait(false)所有等待的操作,以将延续编组到线程池,而不是 ASP.NET 工作线程,但我在避免死锁方面没有任何成功。

我可以解决更深层次的问题吗?我不介意更换组件——我已经创建了自己的IConfiguratioManager<T>. 是否有一个简单的修复方法可以用来防止死锁?

4

2 回答 2

3

@Tragedian 我们针对这个问题进行了这些修复。你能更新一下,看看问题是否仍然存在(我们认为我们已经用 184 修复了它,但你看到我们有 185)。另一位客户使用最新的 nuget 取得了成功。

http://www.nuget.org/packages/Microsoft.IdentityModel.Protocol.Extensions/1.0.2.206221351

https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/185/files

https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/184/files

于 2015-07-06T19:28:08.127 回答
1

我无法评论已接受的答案,但即使使用那个特定的 nuget,问题似乎对我来说仍然存在:/

我发现我需要修改 ConfigurationManager#GetConfigurationAsync 行:

await _refreshLock.WaitAsync(cancel);

_refreshLock.Wait(cancel);

_currentConfiguration = await _configRetriever.GetConfigurationAsync(_metadataAddress, _docRetriever, CancellationToken.None)

_currentConfiguration =  _configRetriever.GetConfigurationAsync(_metadataAddress, _docRetriever, CancellationToken.None).Result;

或者,我在两个调用上都放置了一个 ConfigureAwait(false),并将“GetConfigurationAsync”包装在另一个方法中,该方法用“.Result”调用阻塞,并在一个新的已完成任务中返回它。

如果我这样做,那么对于超过 1 个用户,我将不再发生注销时的死锁(之前的修复解决了单个用户注销的问题。)

但是很明显,这使得“GetConfigurationAsync”方法绝对是同步的:/

于 2016-02-24T17:47:56.297 回答