【问题标题】:Reading from request body in an authorization requirement handler breaks routing从授权要求处理程序中的请求正文中读取会中断路由
【发布时间】:2021-10-13 19:05:12
【问题描述】:

我有一个自定义的安全处理程序和一个端点。它按预期工作。

protected override Task HandleRequirementAsync(
    AuthorizationHandlerContext context,
    AwoRequirement requirement)
{
    HttpRequest request = Accessor.HttpContext.Request;
    string awo = request.Headers
        .SingleOrDefault(a => a.Key == "awo").Value.ToString();
    ...
    bools authorized = ...;

    if (authorized) context.Succeed(requirement);
    else context.Fail();
    return Task.CompletedTask;
}

[Authorize(Policy = "SomePolicy"), HttpPost("something")]
public async Task<IActionResult> Something([FromBody] Thing dto)
{ ... }

现在,我需要检查正文的内容,所以我正在阅读它并分析内容。但是,我注意到通过此添加,不再到达端点。没有例外或任何东西,只是没有命中,就像路线不匹配一样。在调试时,我看到流已用完,因此对流进行断点并再次读取会产生一个空字符串。

protected override Task HandleRequirementAsync( ... )
{
    HttpRequest request = Accessor.HttpContext.Request;
    ...
    using StreamReader stream = new StreamReader(Accessor.HttpContext.Request.Body);
    string body = stream.ReadToEndAsync().Result;
    Thing thing = JsonSerializer.Deserialize<Thing>(body);
    if (thing.IsBad())
      authorized &= fail;
    ...
    return Task.CompletedTask;
}

根据this answer 我应该倒带寻找流的零点,this one 建议也启用缓冲。 (也有建议here,但样本中没有await,这是我的系统必需的,所以我无法正确尝试。)基于此,我登陆了以下内容。

protected override Task HandleRequirementAsync( ... )
{
    HttpRequest request = Accessor.HttpContext.Request;
    ...
    Accessor.HttpContext.Request.EnableBuffering();
    using StreamReader stream 
      = new StreamReader(Accessor.HttpContext.Request.Body);
    string body = stream.ReadToEndAsync().Result;
    Thing thing = JsonSerializer.Deserialize<Thing>(body);
    if (thing.IsBad())
      authorized &= fail;
    ...
    return Task.CompletedTask;
}

现在,返回并重新运行代码确实会再次从流中读取。但是,仍然找不到端点,就像添加上述内容之前一样。但是,如果我从流中删除读数,就会达到它,所以我感觉我仍然以某种方式影响身体读数。

【问题讨论】:

  • 我不确定,但是您是否尝试过使用 StreamReader overload 使 baseStream 保持打开状态并在使用它设置 Accessor.HttpContext.Request.Body.Position = 0 之后将主体流打开并倒带?跨度>
  • 你能倒带吗?如果这是可能的,我会感到非常惊讶,尤其是对于大型流。它不像服务器会在任何地方缓存它,并且 HTTP 不允许重播消息体。
  • 根据链接的答案,应该可以使用Accessor.HttpContext.Request.EnableBuffering()
  • @Neil 看来我可以,尽管只能在处理程序内部。我的流读取了大概 300 字节的正文(甚至不是 1 kb),所以大小应该不是问题。
  • 您似乎试图访问“每条”消息正文中的某种授权数据?这听起来很不标准(出于您现在遇到的原因)。这种方法是“最佳”解决方案,还是您应该以不同的方式看待身份验证?标头(X-AUTH-XXX,或授权等)“更容易”。

标签: c# asp.net-core routes stream asp.net-core-3.1


【解决方案1】:

我猜您需要根据政策检查是否允许用户对提交的资源执行操作 (Thing)。

The way to go about this 是实现一个IAuthorizationHandler,它可以让你通过和检查有问题的资源。

假设我们有一个Post 类:

interface IAuthored
{
    public string AuthorId { get; set; }
}

class Post : IAuthored
{
    public string Title { get; set; }
    public string AuthorId { get; set; }
}

我们希望只允许帖子作者对其进行编辑。

这是控制器。我添加了一个[Authorize],只允许经过身份验证的用户进入。

public class PostController : ControllerBase
{
    private AppDbContext _dbContext;
    private IAuthorizationService _authorizationService;

    public PostController(IAuthorizationService authorizationService, AppDbContext dbContext)
    {
        _authorizationService = authorizationService;
        _dbContext = dbContext;
    }

    [Authorize] // this wouldn't work! [*]
    [HttpPatch("{id}")]
    public async Task<ActionResult> EditPost(string id)
    {
        var post = await _dbContext.Set<Post>().FindAsync(id);

        // oops! any authenticated user can edit this post.

        post.Title = "asd";
        await _dbContext.SaveChangesAsync();

        return Ok();
    }
}

通常,使用简单的策略,我们会使用 [Authorize("my_policy")] 注释操作。但它在这里不起作用,因为[Authorize] 属性在执行到达控制器之前被评估(在授权中间件中)。 ASP.NET Core(或您)无法知道正在处理哪个资源 [*]。

所以我们需要强制授权该操作。我们定义了一个要求,以及一个强制执行它的策略。

// just a marker class
class AuthorRequirement : IAuthorizationRequirement
{
}

services.AddAuthorization(
    options => {
        options.AddPolicy("editor", builder => builder.Requirements.Add(new AuthorRequirement()));
    }
);

然后为这个需求实现一个处理程序。我们可以继承AuthorizationHandler&lt;TRequirement, TResource&gt;AuthorizationHandler&lt;TRequirement&gt;。我选择授权所有实现IAuthored 接口的类。

class AuthorRequirementHandler : AuthorizationHandler<AuthorRequirement, IAuthored> // for a specific
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorRequirement requirement, IAuthored resource)
    {
        var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (resource.AuthorId == userId)
        {
            context.Succeed(requirement);
        }

        // ... let other handlers take a stab at this
        return Task.CompletedTask;
    }
}

这可行,但我们不得不在操作中强制处理授权。

[*]:如果我们可以在资源到达端点之前推断出资源,我们可以将整个操作短路。

让我们创建一个扩展方法,让我们可以多次读取请求正文,并启用请求缓冲。

internal static class HttpRequestExtensions {
    public static async Task<T> ReadAsJsonAsync<T>(this HttpRequest request, JsonSerializerOptions options = null)
    {
        request.Body.Position = 0;
        var result = await request.ReadFromJsonAsync<T>(options);
        // reset the position again to let endpoint middleware read it
        request.Body.Position = 0;
        return result;
    }
}


app.Use((context, next) => {
    context.Request.EnableBuffering(1_000_000);
    return next();
});

app.UseAuthorization();

现在我们可以重写处理程序来读取正文并“以声明方式”执行授权 [^1]。

class AuthorRequirementHandler : AuthorizationHandler<AuthorRequirement>
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public AuthorRequirementHandler(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorRequirement requirement)
    {
        var endpoint = _httpContextAccessor.HttpContext.GetEndpoint();
        var action  = endpoint.Metadata.OfType<ControllerActionDescriptor>().FirstOrDefault();
        // is the action is expecting a post DTO
        if (!action.Parameters.Any(p => p.ParameterType == typeof(Post)))
        {
            return;
        }
        
        var post = await _httpContextAccessor!.HttpContext!.Request.ReadAsJsonAsync<Post>();
        var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (post.AuthorId == userId)
        {
            context.Succeed(requirement);
        }
    }
}

一旦用户被授权,请求就会沿着中间件链到达EndpointMiddleware,它会再次读取和解析请求,并将其委托给控制器操作。


[^1]:我实际上反对这种方法,因为它混合了授权,这是一个业务需求,以及它不应该知道的实现细节(HTTP)。与以前的方法不同,它也是不可测试的。

【讨论】:

  • 曾经让我困惑的是ReadAsJsonAsync&lt;T&gt;(...) 的实现。也许我在流程中遗漏了一些东西,但对我来说,在 HttpContext 的实例上调用它似乎会调用该方法本身,并且一旦出现,它将一次又一次地递归调用自身而没有结束。如果这是一个有意的递归调用,我看不到它返回实际值的基本情况。我错过了什么?
  • 不不,我正在调用不同的方法,请再次检查。这很令人困惑,但我愿意接受更好的命名建议:)
  • 咳咳...我知道这不是该网站的使用方式。有了这个免责声明(并保证不方便的羞耻),我想知道你是否想看看this issue I have with logout。我已经解决了其他所有问题(很大程度上要感谢您的贡献),但最后一部分是不行的。经过几天的研究,我意识到我被困住了。如果你没有时间,我完全理解。我仍然非常感激。
猜你喜欢
  • 2020-05-04
  • 2020-04-27
  • 2014-07-08
  • 1970-01-01
  • 2012-11-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多