Asp.net 基于核心 WebAPI 资源的控制器级别外部授权

Asp.net Core WebAPI Resource based authorization outside controller level

我正在创建一个 Web API,其中的用户具有不同的角色,此外与任何其他应用程序一样,我不希望用户 A 访问用户 B 的资源。如下所示:

Orders/1 (User A)

Orders/2 (User B)

当然,我可以从请求中获取 JWT 并查询数据库以检查该用户是否拥有该订单,但这会使我的控制器操作过于繁重。

这个 example 使用 AuthorizeAttribute 但它似乎太宽泛了,我将不得不为 API 中的所有路由添加大量条件来检查正在访问的路由然后查询数据库进行多次连接,如果请求有效,则返回用户 table 到 return。

Update

For Routes the first line of defense is a security policy which require certain claims.

My question is about the second line of defense that is responsible to make sure users only access their data/resources.

在这种情况下是否有任何标准方法可以采用?

您可以在启动的 ConfigureServices 方法中制定多个策略,包含角色,或者适合您此处示例的名称,如下所示:

AddPolicy("UserA", builder => builder.RequireClaim("Name", "UserA"))

或将 "UserA" 替换为 "Accounting",将 "Name" 替换为 "Role"。
然后按角色限制控制器方法:

[Authorize(Policy = "UserA")

当然这又是在控制器级别,但您不必绕过令牌或数据库。这将为您提供关于什么角色或用户可以使用什么方法的直接指示。

你的说法是错误的,你的设计也是错误的。

Over optimization is the root of all evil

这个link可以概括为"test the performance before claiming it won't work."

使用身份(jwt 令牌或您配置的任何内容)来检查实际用户是否正在访问正确的资源(或者最好只提供它拥有的资源)并不太繁重。 如果它变得很重,说明你做错了什么。 可能是你有大量的同时访问,你只需要缓存一些数据,比如字典 order->ownerid 随着时间的推移会被清除......但情况似乎并非如此。

关于设计:制作一个可以注入的可重用服务,并有一种方法来访问您需要的接受用户的每个资源(IdentityUser,或 jwt 主题,只是用户 ID,或任何您拥有的)

类似于

ICustomerStore 
{
    Task<Order[]> GetUserOrders(String userid);
    Task<Bool> CanUserSeeOrder(String userid, String orderid);
}

相应地实施并使用此 class 系统地检查用户是否可以访问资源。

使用[Authorize]属性称为声明式授权。但它是在执行控制器或动作之前执行的。当您需要基于资源的授权并且文档有作者 属性 时,您必须在授权评估之前从存储中加载文档。这叫命令式授权。

Microsoft Docs 上有一个 post how to deal with imperative authorization in ASP.NET Core。我认为它非常全面,它回答了您关于标准方法的问题。

另外 here 您可以找到代码示例。

我采用的方法是自动将查询限制为当前经过身份验证的用户帐户所拥有的记录。

我使用一个界面来指示哪些数据记录是帐户特定的。

public interface IAccountOwnedEntity
{
    Guid AccountKey { get; set; }
}

并提供一个接口来注入用于识别存储库应定位到哪个帐户的逻辑。

public interface IAccountResolver
{
    Guid ResolveAccount();
}

我今天使用的 IAccountResolver 的实现是基于经过身份验证的用户声明。

public class ClaimsPrincipalAccountResolver : IAccountResolver
{
    private readonly HttpContext _httpContext;

    public ClaimsPrincipalAccountResolver(IHttpContextAccessor httpContextAccessor)
    {
        _httpContext = httpContextAccessor.HttpContext;
    }

    public Guid ResolveAccount()
    {
        var AccountKeyClaim = _httpContext
            ?.User
            ?.Claims
            ?.FirstOrDefault(c => String.Equals(c.Type, ClaimNames.AccountKey, StringComparison.InvariantCulture));

        var validAccoutnKey = Guid.TryParse(AccountKeyClaim?.Value, out var accountKey));

        return (validAccoutnKey) ? accountKey : throw new AccountResolutionException();
    }
}

然后在存储库中,我将所有返回的记录限制为由该帐户拥有。

public class SqlRepository<TRecord, TKey>
    where TRecord : class, IAccountOwnedEntity
{
    private readonly DbContext _dbContext;
    private readonly IAccountResolver _accountResolver;

    public SqlRepository(DbContext dbContext, IAccountResolver accountResolver)
    {
        _dbContext = dbContext;
        _accountResolver = accountResolver;
    }

    public async Task<IEnumerable<TRecord>> GetAsync()
    {
        var accountKey = _accountResolver.ResolveAccount();

        return await _dbContext
                .Set<TRecord>()
                .Where(record => record.AccountKey == accountKey)
                .ToListAsync();
    }


    // Other CRUD operations
}

使用这种方法,我不必记住对每个查询应用我的帐户限制。它只是自动发生。

确保用户A无法查看Order with Id=2(属于用户B).我会做以下两件事之一:

一个: 有一个 GetOrderByIdAndUser(long orderId, string username),当然你从 jwt 中获取用户名。 如果用户不拥有该订单,他将看不到它,并且没有额外的数据库调用。

两个: 首先从数据库中获取订单 GetOrderById(long orderId),然后验证订单的用户名-属性 是否与 jwt 中的登录用户相同。 如果用户不拥有订单抛出异常,return 404 或其他,并且没有额外的数据库调用。

void ValidateUserOwnsOrder(Order order, string username)
{
            if (order.username != username)
            {
                throw new Exception("Wrong user.");
            }
}

您需要回答的第一个问题是 "When I can make this authorisation decision?"。您什么时候真正拥有进行检查所需的信息?

如果您几乎总能确定从路由数据(或其他请求上下文)访问的资源,那么 policy with matching requirement and handler 可能是合适的。当您与明显被资源隔离的数据进行交互时,这种方法效果最好——因为它对诸如列表过滤之类的事情根本没有帮助,在这些情况下您将不得不退回到命令式检查。

如果您在实际检查资源之前无法真正弄清楚用户是否可以 fiddle 使用该资源,那么您几乎只能使用命令式检查。为此有 standard framework,但在我看来,它不如政策框架有用。在某些时候写一个 IUserContext 可能是有价值的,它可以在你查询你的域时注入(所以进入 repos/wherever 你使用 linq),它封装了一些过滤器(IEnumerable<Order> Restrict(this IEnumerable<Order> orders, IUserContext ctx)) .

对于复杂的域,不会有简单的灵丹妙药。如果您使用 ORM,它可能会帮助您 - 但不要忘记您域中的可导航关系将允许代码中断上下文,特别是如果您没有严格尝试保持聚合隔离(myOrder.Items[n].Product.Orderees[notme]...).

上次我这样做时,我成功地对 90% 的情况使用了基于策略的路由方法,但仍然必须对奇怪的列表或复杂的查询进行一些手动命令式检查。使用命令式检查的危险,正如我相信您所知道的,是您忘记了它们。一个潜在的解决方案是在控制器级别应用您的 [Authorize(Policy = "MatchingUserPolicy")],在操作上添加额外的策略 "ISolemlySwearIHaveDoneImperativeChecks",然后在您的 MatchUserRequirementsHandler 中,检查上下文并绕过天真的 user/order 匹配检查命令式检查是否已 'declared'.