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

时间:2020-04-16 20:42:23

标签: c# asp.net-core-webapi asp.net-core-3.1 jwt-auth

除其他任何应用程序外,我不希望用户A访问用户B的资源,而且我将创建具有不同角色的用户的Web API。如下所示:

订单/ 1(用户A

Orders / 2(用户B

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

example使用AuthorizeAttribute,但是它看起来太宽泛,我将不得不为API中的所有路由添加大量条件,以检查正在访问的路由,然后查询数据库以进行多次联接,从而导致返回返回到用户表以返回请求是否有效。

更新

对于路线,第一道防线是一项安全策略, 要求某些声明。

我的问题是关于第二道防线 确保用户仅访问其数据/资源。

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

6 个答案:

答案 0 :(得分:4)

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

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

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
}

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

答案 1 :(得分:3)

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

Microsoft文档上有post,该如何处理ASP.NET Core中的命令性授权。我认为它非常全面,可以回答您有关标准方法的问题。

here可以找到代码示例。

答案 2 :(得分:2)

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

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

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

[Authorize(Policy = "UserA")

当然,这又是在控制器级别上的,但是您不必在令牌或数据库周围乱砍。这将直接指示您什么角色或用户可以使用哪种方法。

答案 3 :(得分:2)

为确保用户A 无法查看ID = 2 的订单(属于用户B )。我会做以下两件事之一:

一个: 有一个GetOrderByIdAndUser(long orderId, string username),当然您要从jwt获取用户名。 如果用户不拥有该订单,那么他将不会看到该订单,也不会进行额外的数据库调用。

两个: 首先从数据库中获取订单GetOrderById(long orderId),然后验证订单的用户名属性是否与jwt中的登录用户相同。 如果用户不拥有Order Throw异常,则返回404或其他任何值,并且不进行额外的数据库调用。

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

答案 4 :(得分:1)

您的陈述是错误的,您也正在设计错误的陈述。

Over optimization is the root of all evil

此链接可以概括为“在声明无法使用之前测试性能”。

使用身份(jwt令牌或您配置的任何内容)来检查实际用户是否正在访问正确的资源(或者可能更好地仅提供其拥有的资源)不是很繁重。 如果变得沉重,则说明您做错了什么。 可能是您有大量的同时访问权限,而您只需要缓存一些数据,例如字典order-> ownerid会随着时间的流逝而被清除……但事实并非如此。

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

类似

from zipfile import ZipFile
import os

zipName = ZipFile(input('Enter zip file name: '), 'w')
fileName = ''

while True:
    fileName = input('Enter file name to zip (enter quit to exit): ')
    if fileName == "quit":
        break
    open(fileName, 'w+').close()
    zipName.write(fileName)
    os.remove(fileName)

相应地实现,并使用此类来严格检查用户是否可以访问资源。

答案 5 :(得分:1)

您需要回答的第一个问题是“何时可以做出此授权决定?”。您何时真正掌握进行检查所需的信息?

如果几乎​​总是可以从路由数据(或其他请求上下文)确定正在访问的资源,那么policy with matching requirement and handler可能是合适的。当您与资源明确隔离的数据进行交互时,这种方法最有效-因为它对列表过滤之类的操作根本无济于事,在这种情况下,您将不得不依靠命令式检查。

如果您在真正检查资源之前不能真正弄清用户是否可以摆弄资源,那么您就非常需要进行强制检查。这里有standard framework,但它不如策略框架有用。在某个时候编写一个IUserContext可能很有价值,可以在查询您的域时将其注入(这样可以在repos /使用linq的任何地方),它封装了一些过滤器(IEnumerable<Order> Restrict(this IEnumerable<Order> orders, IUserContext ctx))。 / p>

对于复杂的域,不会有一个简单的灵丹妙药。如果您使用ORM,它也许可以为您提供帮助-但请不要忘记,您域中的可导航关系将允许代码破坏上下文,尤其是在您不严格尝试将聚合保持隔离的情况下(myOrder.Items [ n] .Product.Orderees [notme] ...)。

上一次,我设法在90%的情况下使用了基于策略的路由方法,但是仍然必须对奇数列表或复杂查询进行一些手动命令式检查。我敢肯定,您知道使用命令式检查的危险是忘记了它们。一种可能的解决方案是在控制器级别应用[Authorize(Policy = "MatchingUserPolicy")],在操作上添加其他策略“ ISolemlySwearIHaveDoneImperativeChecks”,然后在MatchUserRequirementsHandler中检查上下文,如果需要进行检查则绕过幼稚的用户/订单匹配检查被“宣布”。