【问题标题】:Generic authorization with handler处理程序的通用授权
【发布时间】:2018-04-23 16:33:08
【问题描述】:

我正在尝试在我的 ASP.NET Core 2.0 Web 应用中实现授权。

这个应用程序有大约 20 个模型,每个模型都有一个至少实现 CRUD 的控制器。我找到了这些twopages,我喜欢使用处理程序来授权申请的想法。我最初想通过用户实现授权,即用户只有查看/编辑自己实体的权限。我所有的数据库实体都有一个 OwnerId 字段。

我发现的这些示例似乎只适用于一个特定的控制器。

那么,我的问题是:是否可以为所有控制器创建一个授权处理程序?

【问题讨论】:

标签: authorization asp.net-core-2.0


【解决方案1】:

您是否找到了适用于授权处理程序或授权属性的解决方案或解决方法?我的设置和你一模一样。

我试图创建一个通用属性来服务于所有可能的 Entity CRUD 所有者检查,但设计不允许通用属性。

我想出的唯一两个(不令人满意的)解决方案是:

  1. 在控制器操作中,从用户那里获取 ownerId,将其一直转发到您的 CRUD,并在其中包含对 ownerId 的检查。但是,必须为每个控制器中的每个操作复制代码。

    [HttpGet("{id}"]
    public async Task<IActionResult> GetById(int id)
    {
        var stringGuid = User.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
        if (String.IsNullOrWhiteSpace(stringGuid)) return Unauthorized();
        var ownerGuid = new Guid(stringGuid);
    
        var entity = _yourCrudInstance.GetById(id, ownerGuid);
    
        return Ok(entity);
    }
    
  2. 向您的 CRUD 存储库添加一个方法,例如 bool IsOwner(Guid ownerId),并在创建自定义授权处理程序时使用此方法(通过与自定义处理程序一起创建自定义需求)。这消除了控制器中的代码重复,因为您可以使用此自定义授权处理程序创建新策略,因此您可以简单地使用 [Authorize(Policy = "yourOwnershipPolicy")] 装饰每个操作。但是,仍然必须为每个控制器创建一个服务。此外,与解决方案 1 相比,IsOwner(...) 方法增加了一个额外的数据库调用 - 一个用于检查所有权的 db 调用(在授权检查期间)和一个用于实际获取实体的 db 调用(通过控制器操作)。

    [Authorize(Policy = "yourOwnershipPolicy")]
    public async Task<IActionResult> GetById(int id)
    {
        var entity = _yourCrudInstance.GetById(id);
    
        return Ok(entity);
    }
    

我将使用第一个解决方案,直到找到一种方法为我的通用 CRUD 存储库创建通用授权处理,因为人们可能会忘记为新实体创建所需的授权策略,但不能忘记提供参数 ownerId到.GetById(id, ownerGuid),前提是没有重载方法,或者代码无法编译。

更新:

  1. 我找到了第三种解决方案,它能够创建一种通用授权属性。诀窍是使用具体存储库的类型作为授权属性中的输入参数。然而,仍然存在一个限制:必须为每种类型的 Id 复制授权属性,例如 int Id、Guid id 等。但这仍然减少了对 id 类型的重复代码。在大多数情况下,人们只有一种类型的 id,可能是 int 或 Guid。

这里有一些代码展示了我的架构。它被大量总结和编辑,但应该可以成功编译。我的原始代码正在工作和生产中:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

[Route("api/yourcontroller")]
public class YourApiController : Controller
{
    private readonly YourEntityXYZRepository _repo;

    public YourApiController(YourDbContext yourDbContext)
    {
        _repo = new YourEntityXYZRepository(yourDbContext);
    }

    [HttpGet("{id}")]
    [AuthorizeOwnerIntId(typeof(YourEntityXYZRepository), Policy = "YourCustomPolicy")]
    public async Task<IActionResult> GetById(int id)
    {
        var entity = _repo.GetById(id);
        return Ok(entity);
    }
}

// The "generic" authorization attribute for type int id
// Similar authorization attributes for every type of id must be created additionally, for example Guid
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class AuthorizeOwnerIntIdAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    private object _entityRepositoryObject;
    private IAsyncOwnerIntId _entityRepository;
    private readonly Type _TCrudRepository;

    public AuthorizeOwnerIntIdAttribute(Type TCrudRepository)
    {
        _TCrudRepository = TCrudRepository;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var yourDbContext = context.HttpContext.RequestServices.GetService<YourDbContext>();
        _entityRepositoryObject = Activator.CreateInstance(_TCrudRepository, yourDbContext);
        _entityRepository = _entityRepositoryObject as IAsyncOwnerIntId;

        var user = context.HttpContext.User;

        if (!user.Identity.IsAuthenticated)
        {
            // it isn't needed to set unauthorized result 
            // as the base class already requires the user to be authenticated
            // this also makes redirect to a login page work properly
            // context.Result = new UnauthorizedResult();
            return;
        }

        // get entityId from uri
        var idString = context.RouteData.Values["id"].ToString();
        if (!int.TryParse(idString, out var entityId))
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        // get subjectId from user claims
        var ownerIdString = context.HttpContext.User.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
        if (!Guid.TryParse(ownerIdString, out var ownerGuid))
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        if (!_entityRepository.IsEntityOwner(entityId, ownerGuid))
        {
            context.Result = new UnauthorizedResult();
        }
    }
}

// Your concrete repository
public class YourEntityXYZRepository : AsyncCrud<YourEntityXYZ, int>,
    IAsyncOwnerIntId // Note that type concrete IAsyncOwnerIntId is only implemented in concrete repository
{
    public YourEntityXYZRepository(YourDbContext yourDbContext) : base(yourDbContext)
    {

    }
}

// Your generic Crud repository
public abstract class AsyncCrud<TEntity, TId> : IAsyncCrud<TEntity, TId>
    where TEntity : class, IEntityUniqueIdentifier<TId>, IEntityOwner
    where TId : struct
{
    protected YourDbContext YourDbContext;

    public AsyncCrud(YourDbContext yourDbContext)
    {
        YourDbContext = yourDbContext;
    }

    // Note that the following single concrete implementation satisfies both interface members 
    // bool IsEntityOwner(TId id, Guid ownerGuid); from IAsyncCrud<TEntity, TId> and
    // bool IsEntityOwner(int id, Guid ownerGuid); from IAsyncOwnerIntId
    public bool IsEntityOwner(TId id, Guid ownerGuid)
    {
        var entity = YourDbContext.Set<TEntity>().Find(id);
        if (entity != null && entity.OwnerGuid == ownerGuid)
        {
            return true;
        }

        return false;
    }

    // Further implementations (redacted)
    public Task<bool> SaveContext() { throw new NotImplementedException(); }
    public Task<TEntity> Update(TEntity entity){ throw new NotImplementedException(); }
    public Task<TEntity> Create(TEntity entity, Guid ownerGuid) { throw new NotImplementedException(); }
    public Task<bool> Delete(TId id) { throw new NotImplementedException(); }
    public Task<bool> DoesEntityExist(TId id) { throw new NotImplementedException(); }
    public virtual Task<TEntity> GetById(TId id) { throw new NotImplementedException(); }
}

// The interface for the Crud operations
public interface IAsyncCrud<TEntity, TId>
    where TEntity : class, IEntityUniqueIdentifier<TId>
    where TId : struct
{
    bool IsEntityOwner(TId id, Guid ownerGuid);
    Task<bool> DoesEntityExist(TId id);
    Task<TEntity> GetById(TId id);
    Task<TEntity> Create(TEntity entity, Guid ownerGuid);
    Task<TEntity> Update(TEntity entity);
    Task<bool> Delete(TId id);
    Task<bool> SaveContext();
}

// The interface for the concrete type method for int id
// Similar interfaces for every type of id must be created additionally, for example Guid
public interface IAsyncOwnerIntId
{
    bool IsEntityOwner(int id, Guid ownerGuid);
}

// Typical db context
public class YourDbContext : DbContext
{
    public YourDbContext(DbContextOptions<YourDbContext> options) : base(options)
    {

    }

    public DbSet<YourEntityXYZ> YourEntityXYZ { get; set; }
}


public class YourEntityXYZ : IEntityUniqueIdentifier<int>, IEntityOwner
{
    public int Id { get; set; }
    public Guid? OwnerGuid { get; set; }
    // ... Additonal custom properties
}

public interface IEntityUniqueIdentifier<TId>
    where TId : struct
{
    TId Id { get; set; }
}

public interface IEntityOwner
{
    Guid? OwnerGuid { get; set; }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-06-24
    • 2013-02-05
    • 1970-01-01
    • 2019-10-25
    • 1970-01-01
    • 2020-10-28
    • 2020-09-25
    • 1970-01-01
    相关资源
    最近更新 更多