【问题标题】:Sanitize ASP.NET Core's automatic 400 responses清理 ASP.NET Core 的自动 400 响应
【发布时间】:2021-12-08 06:31:26
【问题描述】:

当操作收到错误输入时,运行时的 automatic 400 response 功能会生成一个 ProblemDetails,其中包含如下错误消息 (errors.$[0]):

"The JSON value could not be converted to CompanyName.Foo.Bar. Path: $ | LineNumber: 0 | BytePositionInLine: 3."

我不想泄露实现细节。

如何排除CompanyName.Foo.Bar?

(我使用的是 ASP.NET Core 5,带有 API 控制器,而不是 MVC。)

【问题讨论】:

  • 使用ViewModel 并在内部将其映射到域对象,以便您唯一“泄漏”您的视图模型,即您的应用/ API 的消费者已经知道他们需要提供的内容
  • 我不会真正考虑“泄露实现细节”。您的 API 接受一些 JSON,如果 JSON 与预期格式不匹配,那么告诉他们正确的格式是什么有什么害处?这与泄漏异常消息或配置设置或有关服务器端代码实际实现方式的任何细节不同。
  • 暴露CompanyName.Foo.Bar.CustomerType 肯定会泄露实现细节。
  • @MindSwipe 这是一个不错的想法,但有时你想直接绑定到一些简单的类型,而不需要使用 viewmodel / inputmodel 的仪式,在这种情况下你会遇到这个问题。

标签: c# asp.net-core asp.net-core-5.0


【解决方案1】:

让我与您分享我们在模型绑定错误的情况下自定义响应的实现。

首先,让我们定义一个接口,其中包含一个可以传递给InvalidModelStateResponseFactory的方法:

public interface IModelBindingErrorHandler
{
    IActionResult HandleInvalidModelState(ActionContext context);
}

让我们继续定义两个模型。一个用于记录,另一个用于响应:

public class InvalidInputModel
{
    public string FieldName { get; init; }
    public string[] Errors { get; init; }

    public override string ToString() => $"{FieldName}: {string.Join("; ", Errors)}";
}

public class GlobalErrorModel
{
    public string ErrorMessage { get; init; }
    public string ErrorTracingId { get; init; }
}

如您所见,它们都足够通用,也可以在其他错误处理程序中使用。

现在让我们实现IModelBindingErrorHandler 接口:

public class ModelBindingErrorHandler : IModelBindingErrorHandler
{
    private ILogger<ModelBindingErrorHandler> logger;

    public ModelBindingErrorHandler(ILogger<ModelBindingErrorHandler> logger)
        => this.logger = logger;
    
    public IActionResult HandleInvalidModelState(ActionContext context)
    {
        var modelErrors = context.ModelState
            .Where(stateEntry => stateEntry.Value.Errors.Any())
            .Select(stateEntry => new InvalidInputModel
            {
                FieldName = stateEntry.Key,
                Errors = stateEntry.Value.Errors.Select(error => error.ErrorMessage).ToArray()
            });

        var traceId = Guid.NewGuid();
        logger.LogError("Invalid input model has been captured. ModelState: {modelErrors}, TraceId: {traceId}", modelErrors, traceId);

        return new BadRequestObjectResult(new GlobalErrorModel
        {
            ErrorMessage = "Sorry, the request contains invalid data. Please revise.",
            ErrorTracingId = traceId.ToString()
        });
    }
}
  • 所以,我们在这里基本上收集了所有有价值的信息 (Errors),并且我们正在记录它们
  • 我们使用traceId 将日志条目与响应连接起来
    • 为了简单起见,我在这里使用了Guid.NewGuid() 而不是correlationId

为了方便使用这个实现,这里有两种自我注册的扩展方法:

public static class ModelBindingErrorHandlerRegister
{
    public static IServiceCollection AddModelBinderErrorHandler(this IServiceCollection services)
    {
        return AddModelBinderErrorHandler<ModelBindingErrorHandler>(services);
    }

    public static IServiceCollection AddModelBinderErrorHandler<TImpl>(this IServiceCollection services)
        where TImpl : class, IModelBindingErrorHandler
    {
        services.AddSingleton<IModelBindingErrorHandler, TImpl>();

        var serviceProvider = services.BuildServiceProvider();
        var handler = serviceProvider.GetService<IModelBindingErrorHandler>();
        services.Configure((ApiBehaviorOptions options) =>
            options.InvalidModelStateResponseFactory = handler.HandleInvalidModelState);

        return services;
    }
}
  • 第一种方法注册了上面的实现
  • 第二种方法允许在需要时注册一个自定义方法
  • InvalidModelStateResponseFactory 分配也可以在 PostConfigure 内完成

有了这些,我们可以用一行代码注册一个自定义模型绑定器处理程序:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddModelBinderErrorHandler();
    ...
}

【讨论】:

  • 这很有趣。我的解决方案“解决”了问题,而您的解决方案避免了问题。但是,您的解决方案不会以标准化的 ProblemDetails 响应,但这很容易做到。我认为这是更好的方法。谢谢!
  • @Ionix 每当我们将ProblemDetails 用于面向用户的 API 时,我们都会收到一些反馈,即错误不是很直观。这就是我们提出一个简单模型的原因。我们使用ProblemDetails 进行服务间通信。
【解决方案2】:

想出了一个解决方案。可能会有更好/更简单/更完美的方式。

在Startup.ConfigureServices():

services.Configure<ApiBehaviorOptions>(o => {
  o.InvalidModelStateResponseFactory = actionContext => {

    var problemsDetailsFactory = actionContext.HttpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>();

    var modelState = new ModelStateDictionary();
    foreach (var key in actionContext.ModelState.Keys) {
      var value = actionContext.ModelState[key];
      foreach (var error in value.Errors) {
        var errorMessage = Regex.Replace(error.ErrorMessage, @"^(The JSON value could not be converted)( to .*)(\. Path:.*)$", "$1$3");
        modelState.AddModelError(key, errorMessage);
      }
    }

    var problemDetails = problemsDetailsFactory.CreateValidationProblemDetails(actionContext.HttpContext, modelState, StatusCodes.Status400BadRequest);
    return new BadRequestObjectResult(problemDetails);
  };
});

消毒:

"The JSON value could not be converted to CompanyName.Foo.Bar. Path: $ | LineNumber: 0 | BytePositionInLine: 3."

收件人:

"The JSON value could not be converted. Path: $ | LineNumber: 0 | BytePositionInLine: 3."

【讨论】:

  • 这些信息 (Path: $ | LineNumber: 0 | BytePositionInLine: 3) 对您的客户有用吗?
  • @PeterCsala 不,在我的代码中我也删除了这些。
猜你喜欢
  • 2019-03-14
  • 2019-09-12
  • 2019-09-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多