使用 ASP.NET 身份授权
Authorization With ASP.NET Identity
我不确定我是否在标题上方创造了这个术语,但下面是我的问题摘要。
我使用 ASP.NET WebAPI 和身份验证开发了 REST API。
例如假设我有一家公司 API 位于 http://<myurl>/api/Companies
用于 GET,位于 POST 和 http://<myurl>/api/Companies/{id}
用于 GETBYID、PUT 和 DELETE。
在测试 API 时,我发现一旦用户通过身份验证(通过令牌),s/he 就可以编辑任何公司。
我想限制用户编辑/删除 s/he 所属的公司(以及其他实体)(根据 table 的逻辑结构)。
我心中的解决方案:
我想到的第一个想法是使用 ApplicationUser user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
找到请求操作的用户,然后找到公司 s/he 属于 say by user.CompanyID
并与 Payload if [=45 匹配=] 正在尝试对他所属的同一实体进行编辑/删除或执行其他操作。
是的,对于不同的实体,它将是 user.EntityID
。但这是正确的做法吗?
因为这样,我必须在我所有的控制器编辑和删除操作上写这种代码。
我知道,ASP.NET不能限制逻辑结构上的东西。例如在我的例子中,它无法确定用户是否正在尝试编辑 s/he 所属的公司,因为这对 DB Relation 来说是合乎逻辑的。但是有没有什么我可以开发的 [Authorize]
属性,它包含上述逻辑而不会使控制器混乱?
更新:
不确定每个人是否都了解情况,但这里还有一些故事需要澄清。它是具有 Multi-Tenant 数据库的云端 SaaS 应用程序。因此,每个公司都有自己的一组用户和其他实体,他们不应该能够访问公司以外的实体(根据 table 关系)
API 正在被 Front-End WebApp 使用,所以我现在正在做的是按视图限制。但我担心如果有人只是检查 JS 文件并直接调用 API 怎么办?
我需要一些东西来将用户限制在它的实体内。
正确的,自定义访问规则必须作为业务逻辑的一部分来实现,但是您可以稍微简化一些事情。
例如,如果您的一些实体都有一个 CompanyId
属性 表示它们属于某个特定的公司,那么您可以定义 IHasCompanyOwner
然后有一个检查权限的方法:
interface IHasCompanyOwner {
Int32 CompanyId { get; }
}
public partial class Widget : IHasCompanyOwner {}
public partial class Shop : IHasCompanyOwner {}
private Boolean CanAccessCompanyEntity(IHasCompanyOwner entity) {P
return entity.CompanyId == ((MyUserIdentity)this.User.Identity).CompanyId;
}
public ActionResult GetWidget(Int32 widgetId) {
Widget w = this.database.GetWidget(widgetId);
if( !CanAccessCompanyEntity( w ) ) return this.Http403("You don't have permission to access this Widget");
}
public ActionResult GetShop(Int32 shopId) {
Shop s = this.database.GetShop(shopId);
if( !CanAccessCompanyEntity( s ) ) return this.Http403("You don't have permission to access this Shop");
}
有很多方法可以对此进行切片,但我认为最安全的方法是设置 SQL Views
允许您的用户访问基础表中的数据,过滤数据基于用户/组织/角色/声明。如果你不熟悉视图,你基本上创建你的底层表结构,然后视图被创建为表之上的一层,它们中的每一个都是通过查询生成的,然后你的顶级应用程序可以简单地查询视图。
您可以创建自定义 AuthorizationFilterAttribute
class 以声明方式应用其他过滤器(就像 [Authorize]
的工作方式一样)。以下将为您提供一个 [AuthorizeActive]
过滤器,您可以将其应用于将根据数据库检查用户 ID 以确保它们在每个请求中都是 active
的操作:
using System;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using Microsoft.AspNet.Identity;
public class AuthorizeActiveAttribute : AuthorizationFilterAttribute
{
private readonly IUserService _userService;
public AuthorizeActiveAttribute() {
_userService = Injection.Get<IUserService>();
}
public override void OnAuthorization(HttpActionContext filterContext) {
if (filterContext == null)
throw new ArgumentNullException(nameof(filterContext));
if (SkipAuthorization(filterContext)) return;
var userId = ClaimsPrincipal.Current?.Identity?.GetUserId<int>();
if (userId != null && userId > 0 && _userService.GetUserStatus((int) userId)) {
base.OnAuthorization(filterContext);
}
else {
filterContext.Response = filterContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
}
}
private static bool SkipAuthorization(HttpActionContext filterContext) {
var hasAnonymousAction = filterContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0;
var hasAnonymousController = filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0;
return hasAnonymousAction || hasAnonymousController;
}
}
此外,您可以设置一个包含通用授权逻辑的基础 api 控制器(也许您希望默认情况下授权所有 API):
/// <summary>
/// Base controller for resource related API calls.
/// </summary>
[AuthorizeActive]
public abstract class ResourceController : ApiController {
public bool IsAuthorized => User?.Identity?.IsAuthenticated == true;
public int ActualUserId => GetIntegerClaimValue("actualuserid");
public int UserId => GetIntegerClaimValue("userid");
public int UserTypeId => GetIntegerClaimValue("usertypeid");
public UserTypes UserType => (UserTypes)UserTypeId;
public int ActualOrganizationId => GetIntegerClaimValue("actualorganizationid");
public int OrganizationId => GetIntegerClaimValue("organizationid");
public string Fingerprint => GetClaimValue("fingerprint");
protected string GetClaimValue(string shortName) => (User?.Identity as ClaimsIdentity)?.Claims?.GetValue($"http://schemas.example.com/identity/claims/{shortName}");
protected int GetIntegerClaimValue(string shortName) => Convert.ToInt32(GetClaimValue(shortName));
}
public HomeController : ResourceController {
}
此示例从与用户组织相对应的 .NET 用户身份中提取自定义声明,无论他们是否在模拟等。
现在,您的所有 API 调用都经过授权和保护,默认情况下仅允许在您的数据库中处于活动状态的用户允许匿名访问,用 [AllowAnonymous]
修饰该方法。在继承 ResourceController
的控制器中,您将始终可以访问其任何受保护的属性,从而可以轻松获取用户组织 ID 并在查询中使用它。
我不确定我是否在标题上方创造了这个术语,但下面是我的问题摘要。
我使用 ASP.NET WebAPI 和身份验证开发了 REST API。
例如假设我有一家公司 API 位于 http://<myurl>/api/Companies
用于 GET,位于 POST 和 http://<myurl>/api/Companies/{id}
用于 GETBYID、PUT 和 DELETE。
在测试 API 时,我发现一旦用户通过身份验证(通过令牌),s/he 就可以编辑任何公司。
我想限制用户编辑/删除 s/he 所属的公司(以及其他实体)(根据 table 的逻辑结构)。
我心中的解决方案:
我想到的第一个想法是使用 ApplicationUser user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
找到请求操作的用户,然后找到公司 s/he 属于 say by user.CompanyID
并与 Payload if [=45 匹配=] 正在尝试对他所属的同一实体进行编辑/删除或执行其他操作。
是的,对于不同的实体,它将是 user.EntityID
。但这是正确的做法吗?
因为这样,我必须在我所有的控制器编辑和删除操作上写这种代码。
我知道,ASP.NET不能限制逻辑结构上的东西。例如在我的例子中,它无法确定用户是否正在尝试编辑 s/he 所属的公司,因为这对 DB Relation 来说是合乎逻辑的。但是有没有什么我可以开发的 [Authorize]
属性,它包含上述逻辑而不会使控制器混乱?
更新: 不确定每个人是否都了解情况,但这里还有一些故事需要澄清。它是具有 Multi-Tenant 数据库的云端 SaaS 应用程序。因此,每个公司都有自己的一组用户和其他实体,他们不应该能够访问公司以外的实体(根据 table 关系)
API 正在被 Front-End WebApp 使用,所以我现在正在做的是按视图限制。但我担心如果有人只是检查 JS 文件并直接调用 API 怎么办?
我需要一些东西来将用户限制在它的实体内。
正确的,自定义访问规则必须作为业务逻辑的一部分来实现,但是您可以稍微简化一些事情。
例如,如果您的一些实体都有一个 CompanyId
属性 表示它们属于某个特定的公司,那么您可以定义 IHasCompanyOwner
然后有一个检查权限的方法:
interface IHasCompanyOwner {
Int32 CompanyId { get; }
}
public partial class Widget : IHasCompanyOwner {}
public partial class Shop : IHasCompanyOwner {}
private Boolean CanAccessCompanyEntity(IHasCompanyOwner entity) {P
return entity.CompanyId == ((MyUserIdentity)this.User.Identity).CompanyId;
}
public ActionResult GetWidget(Int32 widgetId) {
Widget w = this.database.GetWidget(widgetId);
if( !CanAccessCompanyEntity( w ) ) return this.Http403("You don't have permission to access this Widget");
}
public ActionResult GetShop(Int32 shopId) {
Shop s = this.database.GetShop(shopId);
if( !CanAccessCompanyEntity( s ) ) return this.Http403("You don't have permission to access this Shop");
}
有很多方法可以对此进行切片,但我认为最安全的方法是设置 SQL Views
允许您的用户访问基础表中的数据,过滤数据基于用户/组织/角色/声明。如果你不熟悉视图,你基本上创建你的底层表结构,然后视图被创建为表之上的一层,它们中的每一个都是通过查询生成的,然后你的顶级应用程序可以简单地查询视图。
您可以创建自定义 AuthorizationFilterAttribute
class 以声明方式应用其他过滤器(就像 [Authorize]
的工作方式一样)。以下将为您提供一个 [AuthorizeActive]
过滤器,您可以将其应用于将根据数据库检查用户 ID 以确保它们在每个请求中都是 active
的操作:
using System;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using Microsoft.AspNet.Identity;
public class AuthorizeActiveAttribute : AuthorizationFilterAttribute
{
private readonly IUserService _userService;
public AuthorizeActiveAttribute() {
_userService = Injection.Get<IUserService>();
}
public override void OnAuthorization(HttpActionContext filterContext) {
if (filterContext == null)
throw new ArgumentNullException(nameof(filterContext));
if (SkipAuthorization(filterContext)) return;
var userId = ClaimsPrincipal.Current?.Identity?.GetUserId<int>();
if (userId != null && userId > 0 && _userService.GetUserStatus((int) userId)) {
base.OnAuthorization(filterContext);
}
else {
filterContext.Response = filterContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
}
}
private static bool SkipAuthorization(HttpActionContext filterContext) {
var hasAnonymousAction = filterContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0;
var hasAnonymousController = filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0;
return hasAnonymousAction || hasAnonymousController;
}
}
此外,您可以设置一个包含通用授权逻辑的基础 api 控制器(也许您希望默认情况下授权所有 API):
/// <summary>
/// Base controller for resource related API calls.
/// </summary>
[AuthorizeActive]
public abstract class ResourceController : ApiController {
public bool IsAuthorized => User?.Identity?.IsAuthenticated == true;
public int ActualUserId => GetIntegerClaimValue("actualuserid");
public int UserId => GetIntegerClaimValue("userid");
public int UserTypeId => GetIntegerClaimValue("usertypeid");
public UserTypes UserType => (UserTypes)UserTypeId;
public int ActualOrganizationId => GetIntegerClaimValue("actualorganizationid");
public int OrganizationId => GetIntegerClaimValue("organizationid");
public string Fingerprint => GetClaimValue("fingerprint");
protected string GetClaimValue(string shortName) => (User?.Identity as ClaimsIdentity)?.Claims?.GetValue($"http://schemas.example.com/identity/claims/{shortName}");
protected int GetIntegerClaimValue(string shortName) => Convert.ToInt32(GetClaimValue(shortName));
}
public HomeController : ResourceController {
}
此示例从与用户组织相对应的 .NET 用户身份中提取自定义声明,无论他们是否在模拟等。
现在,您的所有 API 调用都经过授权和保护,默认情况下仅允许在您的数据库中处于活动状态的用户允许匿名访问,用 [AllowAnonymous]
修饰该方法。在继承 ResourceController
的控制器中,您将始终可以访问其任何受保护的属性,从而可以轻松获取用户组织 ID 并在查询中使用它。