Asp.net-Web-Api

OWIN 中間件中的全域異常處理

  • December 27, 2015

我正在嘗試在建構於 OWIN 中間件(使用 Owin.Host.SystemWeb 的 IIS 主機)之上的 ASP.NET Web API 2.1 項目中創建統一的錯誤處理/報告。目前我使用了一個自定義異常記錄器,它繼承自System.Web.Http.ExceptionHandling.ExceptionLogger並使用 NLog 記錄所有異常,如下程式碼:

public class NLogExceptionLogger : ExceptionLogger
{

   private static readonly Logger Nlog = LogManager.GetCurrentClassLogger();
   public override void Log(ExceptionLoggerContext context)
   {
      //Log using NLog
   } 
}

我想將所有 API 異常的響應正文更改為友好的統一響應,該響應使用System.Web.Http.ExceptionHandling.ExceptionHandler以下程式碼隱藏所有異常詳細資訊:

public class ContentNegotiatedExceptionHandler : ExceptionHandler
{
   public override void Handle(ExceptionHandlerContext context)
   {
       var errorDataModel = new ErrorDataModel
       {
           Message = "Internal server error occurred, error has been reported!",
           Details = context.Exception.Message,
           ErrorReference = context.Exception.Data["ErrorReference"] != null ? context.Exception.Data["ErrorReference"].ToString() : string.Empty,
           DateTime = DateTime.UtcNow
       };

       var response = context.Request.CreateResponse(HttpStatusCode.InternalServerError, errorDataModel);
       context.Result = new ResponseMessageResult(response);
   }
}

當發生異常時,這將為客戶端返回以下響應:

{
 "Message": "Internal server error occurred, error has been reported!",
 "Details": "Ooops!",
 "ErrorReference": "56627a45d23732d2",
 "DateTime": "2015-12-27T09:42:40.2982314Z"
}

現在,如果Api Controller 請求管道中發生任何異常,這一切都很好。

但是在我的情況下,我使用中間件Microsoft.Owin.Security.OAuth來生成不記名令牌,而這個中間件對 Web API 異常處理一無所知,所以例如,如果在方法中拋出了異常,ValidateClientAuthentication我將NLogExceptionLogger不會ContentNegotiatedExceptionHandler知道有關此異常的任何資訊,也不嘗試處理它,我在中使用的範常式式碼AuthorizationServerProvider如下:

public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
   public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
   {
       //Expcetion occurred here
       int x = int.Parse("");

       context.Validated();
       return Task.FromResult<object>(null);
   }

   public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
   {
       if (context.UserName != context.Password)
       {
           context.SetError("invalid_credentials", "The user name or password is incorrect.");
           return;
       }

       var identity = new ClaimsIdentity(context.Options.AuthenticationType);

       identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));

       context.Validated(identity);
   }
}

因此,我將不勝感激在實施以下 2 個問題方面的任何指導:

1 - 創建一個僅處理由 OWIN 中間件生成的異常的全域異常處理程序?我按照這個答案創建了一個用於異常處理的中間件並將其註冊為第一個中間件,並且我能夠記錄源自“OAuthAuthorizationServerProvider”的異常,但我不確定這是否是最佳方法。

2 - 現在,當我在上一步中實現日誌記錄時,我真的不知道如何更改異常的響應,因為我需要為“OAuthAuthorizationServerProvider”中發生的任何異常返回一個標準的 JSON 模型給客戶端。這裡有一個相關的答案,我試圖依賴它,但它沒有用。

這是我的 Startup 類和GlobalExceptionMiddleware我為異常擷取/記錄創建的自定義。缺少的和平正在為任何異常返回統一的 JSON 響應。任何想法將不勝感激。

public class Startup
{
   public void Configuration(IAppBuilder app)
   {
       var httpConfig = new HttpConfiguration();

       httpConfig.MapHttpAttributeRoutes();

       httpConfig.Services.Replace(typeof(IExceptionHandler), new ContentNegotiatedExceptionHandler());

       httpConfig.Services.Add(typeof(IExceptionLogger), new NLogExceptionLogger());

       OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
       {
           AllowInsecureHttp = true,
           TokenEndpointPath = new PathString("/token"),
           AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
           Provider = new AuthorizationServerProvider()
       };

       app.Use<GlobalExceptionMiddleware>();

       app.UseOAuthAuthorizationServer(OAuthServerOptions);
       app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

       app.UseWebApi(httpConfig);
   }
}

public class GlobalExceptionMiddleware : OwinMiddleware
{
   public GlobalExceptionMiddleware(OwinMiddleware next)
       : base(next)
   { }

   public override async Task Invoke(IOwinContext context)
   {
       try
       {
           await Next.Invoke(context);
       }
       catch (Exception ex)
       {
           NLogLogger.LogError(ex, context);
       }
   }
}

好的,所以這比預期的要容易,感謝@Khalid 的提醒,我最終創建了一個名為的 owin 中間件OwinExceptionHandlerMiddleware,該中間件專門用於處理任何 Owin 中間件中發生的任何異常(記錄它並在返回之前操縱響應)客戶端)。

您需要將此中間件註冊為類中的第一個Startup如下所示:

public class Startup
{
   public void Configuration(IAppBuilder app)
   {
       var httpConfig = new HttpConfiguration();

       httpConfig.MapHttpAttributeRoutes();

       httpConfig.Services.Replace(typeof(IExceptionHandler), new ContentNegotiatedExceptionHandler());

       httpConfig.Services.Add(typeof(IExceptionLogger), new NLogExceptionLogger());

       OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
       {
           AllowInsecureHttp = true,
           TokenEndpointPath = new PathString("/token"),
           AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
           Provider = new AuthorizationServerProvider()
       };

       //Should be the first handler to handle any exception happening in OWIN middlewares
       app.UseOwinExceptionHandler();

       // Token Generation
       app.UseOAuthAuthorizationServer(OAuthServerOptions);

       app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

       app.UseWebApi(httpConfig);
   }
}

中使用的程式碼OwinExceptionHandlerMiddleware如下:

using AppFunc = Func<IDictionary<string, object>, Task>;

public class OwinExceptionHandlerMiddleware
{
   private readonly AppFunc _next;

   public OwinExceptionHandlerMiddleware(AppFunc next)
   {
       if (next == null)
       {
           throw new ArgumentNullException("next");
       }

       _next = next;
   }

   public async Task Invoke(IDictionary<string, object> environment)
   {
       try
       {
           await _next(environment);
       }
       catch (Exception ex)
       {
           try
           {

               var owinContext = new OwinContext(environment);

               NLogLogger.LogError(ex, owinContext);

               HandleException(ex, owinContext);

               return;
           }
           catch (Exception)
           {
               // If there's a Exception while generating the error page, re-throw the original exception.
           }
           throw;
       }
   }
   private void HandleException(Exception ex, IOwinContext context)
   {
       var request = context.Request;

       //Build a model to represet the error for the client
       var errorDataModel = NLogLogger.BuildErrorDataModel(ex);

       context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
       context.Response.ReasonPhrase = "Internal Server Error";
       context.Response.ContentType = "application/json";
       context.Response.Write(JsonConvert.SerializeObject(errorDataModel));

   }

}

public static class OwinExceptionHandlerMiddlewareAppBuilderExtensions
{
   public static void UseOwinExceptionHandler(this IAppBuilder app)
   {
       app.Use<OwinExceptionHandlerMiddleware>();
   }
}

引用自:https://stackoverflow.com/questions/34479465