使用 Katana OpenID Connect 中间件解决 OnSendingHeaders 死锁
Solving OnSendingHeaders deadlock with Katana OpenID Connect Middleware
我正在尝试使用 Katana 项目提供的 OpenID Connect 身份验证中间件。
实现中存在一个错误,在这些情况下会导致死锁:
- 运行 在请求具有 thread-affinity 的主机中(例如 IIS)。
- 尚未检索到 OpenID Connect 元数据文档或缓存副本已过期。
- 应用程序调用
SignOut
验证方法。
- 应用程序中发生了导致写入响应流的操作。
死锁的发生是由于身份验证中间件处理来自主机的回调信号 headers 正在发送的方式。问题的根源在于这个方法:
private static void OnSendingHeaderCallback(object state)
{
AuthenticationHandler handler = (AuthenticationHandler)state;
handler.ApplyResponseAsync().Wait();
}
来自Microsoft.Owin.Security.Infrastructure.AuthenticationHandler
只有当返回的 Task
已经完成时,对 Task.Wait()
的调用才是安全的,而在 OpenID Connect 中间件的情况下它还没有完成。
中间件使用 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>
实验性实现。是否有可用于防止死锁的简单修复程序?
@悲剧演员
我们针对此问题采取了这些修复措施。
您能否更新并查看问题是否仍然存在(我们认为我们已经用 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
我无法对已接受的答案发表评论,但即使有了那个特定的 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' 调用并且 returns 它已经在一个新的完成任务。
如果我这样做,那么我将不再为超过 1 个用户发生注销死锁(之前的修复解决了单个用户注销问题。)
然而,这显然使 'GetConfigurationAsync' 方法明显同步:/
我正在尝试使用 Katana 项目提供的 OpenID Connect 身份验证中间件。
实现中存在一个错误,在这些情况下会导致死锁:
- 运行 在请求具有 thread-affinity 的主机中(例如 IIS)。
- 尚未检索到 OpenID Connect 元数据文档或缓存副本已过期。
- 应用程序调用
SignOut
验证方法。 - 应用程序中发生了导致写入响应流的操作。
死锁的发生是由于身份验证中间件处理来自主机的回调信号 headers 正在发送的方式。问题的根源在于这个方法:
private static void OnSendingHeaderCallback(object state)
{
AuthenticationHandler handler = (AuthenticationHandler)state;
handler.ApplyResponseAsync().Wait();
}
来自Microsoft.Owin.Security.Infrastructure.AuthenticationHandler
只有当返回的 Task
已经完成时,对 Task.Wait()
的调用才是安全的,而在 OpenID Connect 中间件的情况下它还没有完成。
中间件使用 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>
实验性实现。是否有可用于防止死锁的简单修复程序?
@悲剧演员 我们针对此问题采取了这些修复措施。 您能否更新并查看问题是否仍然存在(我们认为我们已经用 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
我无法对已接受的答案发表评论,但即使有了那个特定的 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' 调用并且 returns 它已经在一个新的完成任务。
如果我这样做,那么我将不再为超过 1 个用户发生注销死锁(之前的修复解决了单个用户注销问题。)
然而,这显然使 'GetConfigurationAsync' 方法明显同步:/