OWIN中间件中WindowsIdentity.Impersonate()引发异常,是否为中间件问题?
我们在Web应用中使用以下OWIN中间件,根据传入请求模拟用户:
public class SomeMiddleware : OwinMiddleware { public SomeMiddleware(OwinMiddleware next) : base(next) public override async Task Invoke(IOwinContext context) { var identity = context.Authentication.User?.Identity; if(identity is WindowsIdentity windowsIdentity) { using var ctx = windowsIdentity.Impersonate(); await Next.Invoke(context); } } }
管道中的其他中间件使用log4net记录日志,其附加器布局模板包含用户标识。有时Web应用会抛出System.ObjectDisposedException(安全句柄已关闭)和System.ArgumentException(无效模拟令牌)异常,完整栈追踪如下:
System.ObjectDisposedException: Safe handle has been closed at System.Runtime.InteropServices.SafeHandle.DangerousAddRef(Boolean& success) at System.StubHelpers.StubHelpers.SafeHandleAddRef(SafeHandle pHandle, Boolean& success) at Microsoft.Win32.Win32Native.GetTokenInformation(SafeAccessTokenHandle TokenHandle, UInt32 TokenInformationClass, SafeLocalAllocHandle TokenInformation, UInt32 TokenInformationLength, UInt32& ReturnLength) at System.Security.Principal.WindowsIdentity.GetTokenInformation(SafeAccessTokenHandle tokenHandle, TokenInformationClass tokenInformationClass) at System.Security.Principal.WindowsIdentity.get_User() at System.Security.Principal.WindowsIdentity.GetName() at System.Security.Principal.WindowsIdentity.get_Name() at log4net.Core.LoggingEvent.get_Identity() at log4net.Layout.Pattern.IdentityPatternConverter.Convert(TextWriter writer, LoggingEvent loggingEvent) at log4net.Layout.Pattern.PatternLayoutConverter.Convert(TextWriter writer, Object state) at log4net.Util.PatternConverter.Format(TextWriter writer, Object state) at log4net.Layout.PatternLayout.Format(TextWriter writer, LoggingEvent loggingEvent) at log4net.Appender.AppenderSkeleton.RenderLoggingEvent(TextWriter writer, LoggingEvent loggingEvent) at log4net.Appender.TextWriterAppender.Append(LoggingEvent loggingEvent) at log4net.Appender.FileAppender.Append(LoggingEvent loggingEvent) at log4net.Appender.RollingFileAppender.Append(LoggingEvent loggingEvent) at log4net.Appender.AppenderSkeleton.DoAppend(LoggingEvent loggingEvent) ... Invalid token for impersonation - it cannot be duplicated.</Message><StackTrace> at System.Security.Principal.WindowsIdentity.CreateFromToken(IntPtr userToken) at System.Security.Principal.WindowsIdentity..ctor(IntPtr userToken, String authType, Int32 isAuthenticated) at System.Security.Principal.WindowsIdentity..ctor(SafeAccessTokenHandle safeTokenHandle) at System.Security.SecurityContext.CaptureCore(Reader currThreadEC, StackCrawlMark&amp; stackMark) at System.Threading.ExecutionContext.Capture(StackCrawlMark&amp; stackMark, CaptureOptions options) at System.Threading.ExecutionContext.FastCapture() at System.Runtime.CompilerServices.AsyncMethodBuilderCore.GetCompletionAction(Task taskForTracing, MoveNextRunner&amp; runnerToInitialize) at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AwaitUnsafeOnCompleted[TAwaiter,TStateMachine](TAwaiter&amp; awaiter, TStateMachine&amp; stateMachine) --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.AsyncMethodBuilderCore.&lt;&gt;c.&lt;ThrowAsync&gt;b__6_1(Object state) at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state) at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() at System.Threading.ThreadPoolWorkQueue.Dispatch() at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()</StackTrace><ExceptionString>System.ArgumentException: Invalid token for impersonation - it cannot be duplicated. at System.Security.Principal.WindowsIdentity.CreateFromToken(IntPtr userToken) at System.Security.Principal.WindowsIdentity..ctor(IntPtr userToken, String authType, Int32 isAuthenticated) at System.Security.Principal.WindowsIdentity..ctor(SafeAccessTokenHandle safeTokenHandle) at System.Security.SecurityContext.CaptureCore(Reader currThreadEC, StackCrawlMark&amp; stackMark) at System.Threading.ExecutionContext.Capture(StackCrawlMark&amp; stackMark, CaptureOptions options) at System.Threading.ExecutionContext.FastCapture() at System.Runtime.CompilerServices.AsyncMethodBuilderCore.GetCompletionAction(Task taskForTracing, MoveNextRunner&amp; runnerToInitialize) at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AwaitUnsafeOnCompleted[TAwaiter,TStateMachine](TAwaiter&amp; awaiter, TStateMachine&amp; stateMachine) --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.AsyncMethodBuilderCore.&lt;&gt;c.&lt;ThrowAsync&gt;b__6_1(Object state) at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state) at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() at System.Threading.ThreadPoolWorkQueue.Dispatch() at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()</ExceptionString></Exception></TraceRecord> An unhandled exception of type 'System.ArgumentException' occurred in mscorlib.dll
请问该中间件大概率是故障原因吗?我是否找错了排查方向?
这个中间件确实是导致异常的核心原因,你的排查方向没错,问题出在异步场景下的模拟令牌生命周期管理:
令牌提前释放问题
你用using var ctx = windowsIdentity.Impersonate()创建的模拟上下文,会在await Next.Invoke(context)执行完成后立即释放。但异步操作中,后续的日志操作(比如log4net获取用户标识)可能在模拟上下文释放后才执行,此时WindowsIdentity的安全句柄已经被关闭,就会抛出System.ObjectDisposedException。令牌无法复制问题
异步执行时,.NET会捕获ExecutionContext并在线程池线程上恢复,其中包含了模拟的身份令牌。但WindowsIdentity的默认令牌是不可复制的(比如来自IIS的请求令牌),当尝试在另一个线程上使用这个令牌时,就会抛出System.ArgumentException(无效模拟令牌)。
修复方案
方案1:复制令牌并确保模拟上下文覆盖异步操作
复制原令牌以支持跨线程使用,同时确保模拟上下文覆盖所有依赖身份的异步操作:
public override async Task Invoke(IOwinContext context) { var identity = context.Authentication.User?.Identity; if(identity is WindowsIdentity windowsIdentity) { // 复制令牌,确保可以跨线程安全使用 using var duplicatedToken = new SafeAccessTokenHandle(windowsIdentity.Token.DangerousGetHandle()); using var clonedIdentity = new WindowsIdentity(duplicatedToken.DangerousGetHandle()); using var ctx = clonedIdentity.Impersonate(); await Next.Invoke(context); } }
方案2:修改log4net布局,绕过Identity模式
修改log4net的布局模板,不再使用%identity,而是从OWIN上下文手动获取用户标识并传入日志:
<!-- 原布局可能包含 %identity,替换为自定义字段 --> <conversionPattern value="%date [%thread] %-5level %logger - %property{UserName} %message%newline" />
然后在中间件中设置日志属性:
log4net.LogicalThreadContext.Properties["UserName"] = context.Authentication.User?.Identity.Name;
方案3:禁用ExecutionContext的身份捕获
如果不需要跨线程保留模拟身份,可以在执行异步操作前禁用身份捕获:
public override async Task Invoke(IOwinContext context) { var identity = context.Authentication.User?.Identity; if(identity is WindowsIdentity windowsIdentity) { using var ctx = windowsIdentity.Impersonate(); // 禁用ExecutionContext捕获身份,避免后续线程尝试使用已释放的令牌 using (ExecutionContext.SuppressFlow()) { await Next.Invoke(context); } } }
内容的提问来源于stack exchange,提问作者Józef Podlecki

