Asp.net Core WebAPI 基于资源的控制器级别之外的授权
Posted
技术标签:
【中文标题】Asp.net Core WebAPI 基于资源的控制器级别之外的授权【英文标题】:Asp.net Core WebAPI Resource based authorization outside controller level 【发布时间】:2020-07-30 06:01:28 【问题描述】:我正在创建一个具有不同角色的用户的 Web API,此外,与任何其他应用程序一样,我不希望用户 A 访问用户 B 的资源。如下:
Orders/1(用户 A)
Orders/2(用户 B)
当然,我可以从请求中获取 JWT 并查询数据库以检查该用户是否拥有该订单,但这会使我的控制器操作过于繁重。
这个example 使用 AuthorizeAttribute 但它似乎太宽泛了,我必须为 API 中的所有路由添加大量条件以检查正在访问哪个路由,然后查询数据库进行几个连接,这些连接返回如果请求有效,则返回 users 表。
更新
对于路由来说,第一道防线是一个安全策略,它 需要某些声明。
我的问题是关于第二道防线 确保用户只访问他们的数据/资源。
在这种情况下是否可以采取任何标准方法?
【问题讨论】:
您可以使用类似“users/userId/orders/orderId”的路由并使用类似“where OwnerId = @userId and OrderId = @orderId”的SQL查询。另外添加授权过滤器检查“userId”路由参数值是否与经过身份验证的用户相同。 @SergeyVishnevsky 嗯,这是一个想法,但是当我可以从 httpContext 中获取 UserId 时,我最终会在每条路由中使用用户 ID,并且我将不得不添加自定义路由对于不拥有订单的管理员。我正在尝试找到一种更清洁的标准方法。 你到底想达到什么目的?您想允许用户只获得他的订单吗?或者您想为不同的订单创建用户角色? 正如我所见,基于策略的授权与多个处理程序 docs.microsoft.com/en-us/aspnet/core/security/authorization/… 可以提供帮助。您可以使用“ownerId”路由参数作为所有路由的资源所有者的指示符,并为授权要求添加两个或多个 AuthorizationHandler。第一个检查“当前用户Id等于”ownerId“路由值”,第二个检查“当前用户是admin”。如果这些处理程序之一批准了该要求,则请求被授权。 @OlegKyrylchuk 请查看更新后的问题 【参考方案1】:我采用的方法是自动将查询限制为当前经过身份验证的用户帐户拥有的记录。
我使用一个界面来指示哪些数据记录是特定于帐户的。
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
使用这种方法,我不必记住对每个查询都应用我的帐户限制。它只是自动发生。
【讨论】:
在这种情况下,所有实体都实现了 IAccountOwnedEntity ?那么我在所有表中都有用户 ID 吗? 是的,没错。理论上,如果记录特定于某个帐户,它们将在数据库中绑定在一起。 这对我来说似乎过于工程......显然它取决于一个人可能拥有的许多实体以及一个人有多少不同的查询......但它是两个类的接口之一 问题还指出“当然,我可以从请求中获取 JWT 并查询数据库以检查该用户是否拥有该订单,但这会使我的控制器操作过于繁重。”这个答案没有解决这个问题,我的也没有,但至少我解释了为什么...... 此外,这种方法往往会使您的项目更加僵化,设置...现实世界的场景不能全部符合所有者定义,您可能需要创建者、编辑者、管理员,管理这些实体的公司...我不会采用这种方法,只要我的两分钱...【参考方案2】:使用[Authorize]
属性称为声明式授权。但它是在控制器或动作执行之前执行的。当您需要基于资源的授权并且文档具有作者属性时,您必须在授权评估之前从存储中加载文档。这叫做命令式授权。
Microsoft Docs 上有一个post 如何处理 ASP.NET Core 中的命令式授权。我认为它非常全面,它回答了您关于标准方法的问题。
也可以here 找到代码示例。
【讨论】:
我的资源虽然可以是文档,但也可以是数据库记录。 @MozartAlKhateeb 是的,资源可以是任何数据库实体。【参考方案3】:确保用户 A 无法查看 Id=2 的订单(属于 用户 B)。我会做以下两件事之一:
一个:
有一个GetOrderByIdAndUser(long orderId, string username)
,当然你从 jwt 获取用户名。
如果用户不拥有该订单,他将不会看到它,也没有额外的 db-call。
两个:
首先从数据库中获取订单GetOrderById(long orderId)
,然后验证订单的用户名属性是否与 jwt 中的登录用户相同。
如果用户不拥有订单 Throw 异常,则返回 404 或其他什么,并且没有额外的 db-call。
void ValidateUserOwnsOrder(Order order, string username)
if (order.username != username)
throw new Exception("Wrong user.");
【讨论】:
【参考方案4】:您可以在启动的 ConfigureServices 方法中制定多个策略,包括角色,或者根据您的示例,这里的名称,如下所示:
AddPolicy("UserA", builder => builder.RequireClaim("Name", "UserA"))
或将“UserA”替换为“Accounting”,将“Name”替换为“Role”。 然后按角色限制控制器方法:
[Authorize(Policy = "UserA")
当然,这又是在控制器级别,但您不必绕过令牌或数据库。这将为您提供有关哪些角色或用户可以使用哪种方法的直接指示。
【讨论】:
请查看更新后的问题,角色不保证用户 A 不能访问用户 B 的数据,也许可以以不同的方式使用策略来考虑当前会话的用户在其验证中,我会研究. 那么另一层取决于您要定义数据属于谁的位置。例如,您可以有一个持久层,它将用户令牌作为附加参数来检查保存在数据库中的用户与该数据。 [Authorize] 上的策略名称必须是 const 值,这意味着您必须为要添加的每个用户都有一个新的 const 值。这似乎不会很好地扩展到超过 1 或 2 个用户。 @MattHensley 这正是我添加“角色”想法的原因。只需将“UserA”放在那里以在技术上适合 OP 的问题。【参考方案5】:你的陈述是错误的,你的设计也是错误的。
Over optimization is the root of all evil
这个链接可以概括为“在声称它不起作用之前测试性能。”
使用身份(jwt 令牌或您配置的任何内容)来检查实际用户是否正在访问正确的资源(或者最好只提供其拥有的资源)并不太繁重。 如果它变得沉重,那么你做错了什么。 可能是你有大量的同时访问,你只需要缓存一些数据,比如字典 order->ownerid 会随着时间的推移而被清除......但情况似乎并非如此。
关于设计:创建一个可以被注入的可重用服务,并有一个方法来访问你需要的每个接受用户的资源(IdentityUser,或 jwt 主题,只是用户 ID,或任何你拥有的)
类似
ICustomerStore
Task<Order[]> GetUserOrders(String userid);
Task<Bool> CanUserSeeOrder(String userid, String orderid);
相应地实现并使用这个类来系统地检查用户是否可以访问资源。
【讨论】:
这个问题的目的是了解标准方法并指出问题所在,这个 API 仍在建设中,过去我曾经将用户 ID 与查询一起发送到数据库。那么关于 ICustomerStore 是 IService 还是 IRepo 负责查询数据? 这取决于...我的经验是按照一些模式尽可能快地完成工作...然后重构您认为需要额外关注的内容。所以不要放太多层。决定并完成一些测试的好方法是以测试的形式编写请求,这将使您对需要多少层有所了解 你使用一些 orm 吗?像实体框架?我通常将这些商店设计为实体框架(db)和控制器/webapi之间的一个层,因此后者不必了解任何数据源,如果您有这种冲动,用模拟交换商店也应该可以工作要访问数据库,只需向商店添加一些方法 是的,我使用实体框架,根据您的描述,这可能是一个服务层,将数据库查询与业务逻辑一起封装。 绝对是的,数据可以来自任何来源,您只需设计一个套接字。然后,如果速度不够快,您将为您的服务设计一个缓存,可以是数据库中的另一个较小的表,也可以是内存中的任何您喜欢和工作的缓存。【参考方案6】:您需要回答的第一个问题是“我什么时候可以做出授权决定?”。您何时真正拥有进行检查所需的信息?
如果您几乎总能确定从路由数据(或其他请求上下文)访问的资源,那么policy with matching requirement and handler 可能是合适的。当您与明显被资源隔离的数据进行交互时,这种方法最有效——因为它对过滤列表等事情毫无帮助,在这些情况下您将不得不退回到命令式检查。
如果在您真正检查过资源之前,您无法真正确定用户是否可以摆弄资源,那么您几乎会被命令式检查所困。对此有standard framework,但它不像政策框架那样有用。在某些时候编写一个 IUserContext
可能很有价值,它可以在您查询域时注入(因此注入到 repos/无论您在哪里使用 linq),它封装了其中一些过滤器 (IEnumerable<Order> Restrict(this IEnumerable<Order> orders, IUserContext ctx)
)。
对于复杂的域,不会有简单的灵丹妙药。如果您使用 ORM,它可能会为您提供帮助 - 但不要忘记域中的可导航关系将允许代码破坏上下文,特别是如果您没有严格尝试保持聚合隔离(myOrder.Items[ n].Product.Orderees[notme]...).
上次我这样做时,我设法在 90% 的情况下使用了基于策略的路由方法,但仍然必须对奇怪的列表或复杂的查询进行一些手动命令式检查。我相信您知道,使用命令式检查的危险在于您忘记了它们。一个潜在的解决方案是在控制器级别应用你的[Authorize(Policy = "MatchingUserPolicy")]
,在操作上添加一个额外的策略“ISolemlySwearIHaveDoneImperativeChecks”,然后在你的 MatchUserRequirementsHandler 中,检查上下文并绕过天真的用户/订单匹配检查,如果命令检查已经'宣布'。
【讨论】:
以上是关于Asp.net Core WebAPI 基于资源的控制器级别之外的授权的主要内容,如果未能解决你的问题,请参考以下文章
在 ASP.NET Core WebAPI 中实现 JSON 合并补丁
选择 webApi 模板时如何将 ASP.Net 身份添加到 Asp.Net Core?
ASP.NET Core 5.0 WebAPI 中的多种身份验证方案