首页 > 解决方案 > 从授权要求处理程序中的请求正文中读取会中断路由

问题描述

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

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;
}

根据这个答案,我应该倒带寻找流的零点,这个建议也启用缓冲。(这里也有建议,但示例中没有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;
}

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

标签: c#asp.net-coreroutesstreamasp.net-core-3.1

解决方案


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

解决这个问题的方法是实现一个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<TRequirement, TResource>AuthorizationHandler<TRequirement>。我选择授权所有实现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) 混合在一起。与以前的方法不同,它也是不可测试的。


推荐阅读