2017-01-05 1 views
22

Dans mon projet WebAPI, j'utilise Owin.Security.OAuth pour ajouter l'authentification JWT. intérieur GrantResourceOwnerCredentials de mon OAuthProvider J'installe des erreurs en utilisant la ligne ci-dessous:WebAPi - formatage des messages d'erreur Unify à partir de ApiController et OAuthAuthorizationServerProvider

context.SetError("invalid_grant", "Account locked."); 

cela est retourné au client comme:

{ 
    "error": "invalid_grant", 
    "error_description": "Account locked." 
} 

après que l'utilisateur est authentifié et il essaie de faire la demande « normale » à un de mes contrôleurs, il passe en dessous de réponse lorsque le modèle est valide (en utilisant FluentValidation):

{ 
    "message": "The request is invalid.", 
    "modelState": { 
    "client.Email": [ 
     "Email is not valid." 
    ], 
    "client.Password": [ 
     "Password is required." 
    ] 
    } 
} 

Les deux demandes reviennent 400 Bad Request, mais parfois vous devez regarder pour le champ error_description et parfois pour message

I was able to create message de réponse personnalisée, mais cela ne vaut que pour les résultats que je suis de retour.

Ma question est: est-il possible de remplacer message par error en réponse qui est retournée par ModelValidatorProviders et dans d'autres endroits?

J'ai lu environ ExceptionFilterAttribute mais je ne sais pas si c'est un bon point de départ. FluentValidation ne devrait pas être un problème, car tout ce qu'il fait est d'ajouter des erreurs à ModelState.

EDIT:
La prochaine chose que je suis en train de fixer est convention de nommage incohérente dans les données renvoyées à travers WebAPI - lors du retour d'erreur de OAuthProvider nous avons error_details, mais lors du retour BadRequest avec ModelState (de ApiController), nous avons modelState . Comme vous pouvez le voir d'abord utilise snake_case et la seconde camelCase.

+0

Lorsque vous créez votre objet httpError, pourquoi utilisez-vous des propriétés personnalisées? La classe HttpError a des propriétés qui imitent les vôtres: https://msdn.microsoft.com/en-us/library/system.web.http.httperror(v=vs.118).aspx – returnsvoid

+0

@returnsvoid désolé pour réponse tardive. 'HttpError' a' Message' et 'MessageDescription' mais lors de l'utilisation de' SetError' dans 'OAuthAuthorizationServerProvider', vous définissez' Error' et 'ErrorDescription'. Je voudrais avoir le même nom pour la propriété error (dans peut être Error od Message, cela n'a pas d'importance) alors quand je retourne des informations que quelque chose s'est mal passé, l'utilisateur vérifiera toujours la propriété unique. J'espère que cela clarifier un peu ma question – Misiu

+0

Utilisez-vous OAuthProvider personnalisé? –

Répondre

0

est-il possible de remplacer un message par erreur en réponse qui est retourné par ModelValidatorProviders

Nous pouvons utiliser surchargées SetError de le faire autrement, remplacer erreur avec le message.

BaseValidatingContext<TOptions>.SetError Method (String) 

Marques ce contexte ne pas validé par l'application et attribue diverses propriétés d'informations d'erreur. HasError devient true et IsValidated devient false à la suite de l'appel.

string msg = "{\"message\": \"Account locked.\"}"; 
context.SetError(msg); 
Response.StatusCode = 400; 
context.Response.Write(msg); 
+0

Merci pour une réponse aussi rapide, mais ce n'est pas ce que je voulais dire. 'OAuthGrantCustomExtensionContext.SetError' renvoie une réponse correcte (il contient les champs' error' et 'error_details'). Ce dont j'ai besoin, c'est de retourner 'BadRequest' à partir de' ApiController', mais au lieu du champ 'message' dans le fichier JSON retourné, j'aimerais avoir un champ' error'. J'espère que cela clarifier un peu ma question. – Misiu

6

RÉPONSE À JOUR (utilisation Middleware)

Depuis l'API Web idée de gestionnaire délégante d'origine signifiait qu'il ne serait pas assez tôt dans le pipeline comme le middleware OAuth alors un middleware personnalisé doit être créé ...

public static class ErrorMessageFormatter { 

    public static IAppBuilder UseCommonErrorResponse(this IAppBuilder app) { 
     app.Use<JsonErrorFormatter>(); 
     return app; 
    } 

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

     public override async Task Invoke(IOwinContext context) { 
      var owinRequest = context.Request; 
      var owinResponse = context.Response; 
      //buffer the response stream for later 
      var owinResponseStream = owinResponse.Body; 
      //buffer the response stream in order to intercept downstream writes 
      using (var responseBuffer = new MemoryStream()) { 
       //assign the buffer to the resonse body 
       owinResponse.Body = responseBuffer; 

       await Next.Invoke(context); 

       //reset body 
       owinResponse.Body = owinResponseStream; 

       if (responseBuffer.CanSeek && responseBuffer.Length > 0 && responseBuffer.Position > 0) { 
        //reset buffer to read its content 
        responseBuffer.Seek(0, SeekOrigin.Begin); 
       } 

       if (!IsSuccessStatusCode(owinResponse.StatusCode) && responseBuffer.Length > 0) { 
        //NOTE: perform your own content negotiation if desired but for this, using JSON 
        var body = await CreateCommonApiResponse(owinResponse, responseBuffer); 

        var content = JsonConvert.SerializeObject(body); 

        var mediaType = MediaTypeHeaderValue.Parse(owinResponse.ContentType); 
        using (var customResponseBody = new StringContent(content, Encoding.UTF8, mediaType.MediaType)) { 
         var customResponseStream = await customResponseBody.ReadAsStreamAsync(); 
         await customResponseStream.CopyToAsync(owinResponseStream, (int)customResponseStream.Length, owinRequest.CallCancelled); 
         owinResponse.ContentLength = customResponseStream.Length; 
        } 
       } else { 
        //copy buffer to response stream this will push it down to client 
        await responseBuffer.CopyToAsync(owinResponseStream, (int)responseBuffer.Length, owinRequest.CallCancelled); 
        owinResponse.ContentLength = responseBuffer.Length; 
       } 
      } 
     } 

     async Task<object> CreateCommonApiResponse(IOwinResponse response, Stream stream) { 

      var json = await new StreamReader(data).ReadToEndAsync(); 

      var statusCode = ((HttpStatusCode)response.StatusCode).ToString(); 
      var responseReason = response.ReasonPhrase ?? statusCode; 

      //Is this a HttpError 
      var httpError = JsonConvert.DeserializeObject<HttpError>(json); 
      if (httpError != null) { 
       return new { 
        error = httpError.Message ?? responseReason, 
        error_description = (object)httpError.MessageDetail 
        ?? (object)httpError.ModelState 
        ?? (object)httpError.ExceptionMessage 
       }; 
      } 

      //Is this an OAuth Error 
      var oAuthError = Newtonsoft.Json.Linq.JObject.Parse(json); 
      if (oAuthError["error"] != null && oAuthError["error_description"] != null) { 
       dynamic obj = oAuthError; 
       return new { 
        error = (string)obj.error, 
        error_description = (object)obj.error_description 
       }; 
      } 

      //Is this some other unknown error (Just wrap in common model) 
      var error = JsonConvert.DeserializeObject(json); 
      return new { 
       error = responseReason, 
       error_description = error 
      }; 
     } 

     bool IsSuccessStatusCode(int statusCode) { 
      return statusCode >= 200 && statusCode <= 299; 
     } 
    } 
} 

... et enregistré au début de la canalisation avant intergiciels d'authentification et les gestionnaires api web sont ajoutés.

public class Startup { 
    public void Configuration(IAppBuilder app) { 

     app.UseResponseEncrypterMiddleware(); 

     app.UseRequestLogger(); 

     //...(after logging middle ware) 
     app.UseCommonErrorResponse(); 

     //... (before auth middle ware) 

     //...code removed for brevity 
    } 
} 

Cet exemple est juste un début de base. Il devrait être assez simple pour étendre ce point de départ.

Bien que dans cet exemple le modèle commun ressemble à ce qui est renvoyé par OAuthProvider, n'importe quel modèle d'objet commun peut être utilisé.

Testé avec quelques tests unitaires en mémoire et grâce à TDD a pu le faire fonctionner.

[TestClass] 
public class UnifiedErrorMessageTests { 
    [TestMethod] 
    public async Task _OWIN_Response_Should_Pass_When_Ok() { 
     //Arrange 
     var message = "\"Hello World\""; 
     var expectedResponse = "\"I am working\""; 

     using (var server = TestServer.Create<WebApiTestStartup>()) { 
      var client = server.HttpClient; 
      client.DefaultRequestHeaders.Accept.Clear(); 
      client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 

      var content = new StringContent(message, Encoding.UTF8, "application/json"); 

      //Act 
      var response = await client.PostAsync("/api/Foo", content); 

      //Assert 
      Assert.IsTrue(response.IsSuccessStatusCode); 

      var result = await response.Content.ReadAsStringAsync(); 

      Assert.AreEqual(expectedResponse, result); 
     } 
    } 

    [TestMethod] 
    public async Task _OWIN_Response_Should_Be_Unified_When_BadRequest() { 
     //Arrange 
     var expectedResponse = "invalid_grant"; 

     using (var server = TestServer.Create<WebApiTestStartup>()) { 
      var client = server.HttpClient; 
      client.DefaultRequestHeaders.Accept.Clear(); 
      client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 

      var content = new StringContent(expectedResponse, Encoding.UTF8, "application/json"); 

      //Act 
      var response = await client.PostAsync("/api/Foo", content); 

      //Assert 
      Assert.IsFalse(response.IsSuccessStatusCode); 

      var result = await response.Content.ReadAsAsync<dynamic>(); 

      Assert.AreEqual(expectedResponse, (string)result.error_description); 
     } 
    } 

    [TestMethod] 
    public async Task _OWIN_Response_Should_Be_Unified_When_MethodNotAllowed() { 
     //Arrange 
     var expectedResponse = "Method Not Allowed"; 

     using (var server = TestServer.Create<WebApiTestStartup>()) { 
      var client = server.HttpClient; 
      client.DefaultRequestHeaders.Accept.Clear(); 
      client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 

      //Act 
      var response = await client.GetAsync("/api/Foo"); 

      //Assert 
      Assert.IsFalse(response.IsSuccessStatusCode); 

      var result = await response.Content.ReadAsAsync<dynamic>(); 

      Assert.AreEqual(expectedResponse, (string)result.error); 
     } 
    } 

    [TestMethod] 
    public async Task _OWIN_Response_Should_Be_Unified_When_NotFound() { 
     //Arrange 
     var expectedResponse = "Not Found"; 

     using (var server = TestServer.Create<WebApiTestStartup>()) { 
      var client = server.HttpClient; 
      client.DefaultRequestHeaders.Accept.Clear(); 
      client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 

      //Act 
      var response = await client.GetAsync("/api/Bar"); 

      //Assert 
      Assert.IsFalse(response.IsSuccessStatusCode); 

      var result = await response.Content.ReadAsAsync<dynamic>(); 

      Assert.AreEqual(expectedResponse, (string)result.error); 
     } 
    } 

    public class WebApiTestStartup { 
     public void Configuration(IAppBuilder app) { 

      app.UseCommonErrorMessageMiddleware(); 

      var config = new HttpConfiguration(); 
      config.Routes.MapHttpRoute(
       name: "DefaultApi", 
       routeTemplate: "api/{controller}/{id}", 
       defaults: new { id = RouteParameter.Optional } 
      ); 

      app.UseWebApi(config); 
     } 
    } 

    public class FooController : ApiController { 
     public FooController() { 

     } 
     [HttpPost] 
     public IHttpActionResult Bar([FromBody]string input) { 
      if (input == "Hello World") 
       return Ok("I am working"); 

      return BadRequest("invalid_grant"); 
     } 
    } 
} 

ORIGINAL RÉPONSE (utilisation DelegatingHandler)

Pensez à utiliser un DelegatingHandler

Je cite un article trouvé en ligne.

Les gestionnaires de délégation sont extrêmement utiles pour les problèmes de coupe transversale. Ils se connectent aux étapes très en amont et très en retard du pipeline de demande-réponse , ce qui les rend idéaux pour manipuler la réponse juste avant qu'elle ne soit renvoyée au client.

Cet exemple est une tentative simplifiée au message d'erreur unifié pour HttpError réponses

public class HttpErrorHandler : DelegatingHandler { 

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { 
     var response = await base.SendAsync(request, cancellationToken); 

     return NormalizeResponse(request, response); 
    } 

    private HttpResponseMessage NormalizeResponse(HttpRequestMessage request, HttpResponseMessage response) { 
     object content; 
     if (!response.IsSuccessStatusCode && response.TryGetContentValue(out content)) { 

      var error = content as HttpError; 
      if (error != null) { 

       var unifiedModel = new { 
        error = error.Message, 
        error_description = (object)error.MessageDetail ?? error.ModelState 
       }; 

       var newResponse = request.CreateResponse(response.StatusCode, unifiedModel); 

       foreach (var header in response.Headers) { 
        newResponse.Headers.Add(header.Key, header.Value); 
       } 

       return newResponse; 
      } 

     } 
     return response; 
    } 
} 

Bien que cet exemple est très basique, il est trivial de l'étendre pour répondre à vos besoins personnalisés.

Maintenant, il est juste une question d'ajouter le gestionnaire à la canalisation

public static class WebApiConfig { 
    public static void Register(HttpConfiguration config) { 

     config.MessageHandlers.Add(new HttpErrorHandler()); 

     // Other code not shown... 
    } 
} 

Les gestionnaires de messages sont appelés dans le même ordre qu'ils apparaissent dans MessageHandlers collection. Parce qu'ils sont imbriqués, le message de réponse se déplace dans l'autre sens. C'est-à-dire que le dernier gestionnaire est le premier à recevoir le message de réponse .

Source: HTTP Message Handlers in ASP.NET Web API

+0

Merci beaucoup pour votre réponse. Je vais essayer ça tout de suite. Dans mon application, j'utilise Middleware pour enregistrer les demandes et les réponses. Si j'ajoute DelegatingHandler cela fonctionnera-t-il avant Middleware ou après? Inside Middleware Invoke méthode J'obtiens tous les paramètres de la requête, puis j'appelle 'Next.Invoke' et j'obtiens tous les paramètres de la réponse. Idéalement, j'aimerais que DelegatingHandler modifie la réponse avant de la connecter. – Misiu

+0

Tout dépend de l'ordre dans lequel vous enregistrez vos gestionnaires et middleware. vous devrez donc l'enregistrer après le middleware de consignation afin que le journal reçoive la réponse après le gestionnaire à la sortie du pipeline. – Nkosi

+0

La meilleure chose que je peux faire est d'ajouter votre code et vérifier si tout fonctionnera comme prévu. Je reviendrai vers vous dès que j'aurai vérifié. J'espère que mes middlewares fonctionneront bien. – Misiu