12

Let's say that you have a .net web api with a GetResource(int resourceId) action. This action (with the specified id) should only be authorized for a user associated with that id (the resource could for instance be a blogpost written by the user).

This could be solved in many ways, but an example is given below.

    public Resource GetResource(int id)
    {
        string name = Thread.CurrentPrincipal.Identity.Name;
        var user = userRepository.SingleOrDefault(x => x.UserName == name);
        var resource = resourceRepository.Find(id);

        if (resource.UserId != user.UserId)
        {
            throw new HttpResponseException(HttpStatusCode.Unauthorized);
        }

        return resource;
    }

where the user has been authenticated by some sort of mechanicm.

Now, let's say that I also want a user of, for instance, an admin type, to be authorized to consume the endpoint (with the same id). This user does not have any direct relation to the resource but does have authorization because of it's type (or role). This could be solved by just checking if the user is of admin type and the return the resource.

Is there any way to centralize this in a way so that I don't have to write authorization code in every action?

Edit Based on the answers I think I have to clarify my question.

What I am really after is some mechanism that makes it possible to have resource based authorization, but at the same time allow some users to also consume the same endpoint and the same resource. The action below would solve this for this specific endpoint and for this specific Role (Admin).

    public Resource GetResource(int id)
    {
        string name = Thread.CurrentPrincipal.Identity.Name;
        var user = userRepository.SingleOrDefault(x => x.UserName == name);
        var resource = resourceRepository.Find(id);

        if (!user.Roles.Any(x => x.RoleName == "Admin" || resource.UserId != user.UserId)
        {
            throw new HttpResponseException(HttpStatusCode.Unauthorized);
        }

        return resource;
    }

The thing I am after is some generic way to solve this problem so that I don't have to write two different endpoints with the same purpose or write resource specific code in every endpoint.

4

5 回答 5

4

对于基于资源的授权,我建议使用基于声明的身份并将用户 ID 作为声明嵌入。编写一个扩展方法来从身份中读取声明。所以示例代码将如下所示:

public Resource GetResource(int id)
{
     var resource = resourceRepository.Find(id);
    if (resource.UserId != User.Identity.GetUserId())
    {
        throw new HttpResponseException(HttpStatusCode.Unauthorized);
    }

    return resource;
}

如果您想进一步简化代码,您可以编写一个 UserRepository,它知道用户数据和资源存储库来集中代码。代码将如下所示:

public Resource GetResource(int id)
{
    return User.Identity.GetUserRepository().FindResource(id);
}

对于基于角色的授权,AuthorizeAttribute将是处理它的最佳位置,您最好为此使用单独的操作或控制器。

[Authorize(Roles = "admin")]
public Resource GetResourceByAdmin(int id)
{
    return resourceRepository.Find(id);
}

[编辑] 如果 OP 确实想使用一个操作来处​​理不同类型的用户,我个人更喜欢使用用户存储库工厂。操作代码将是:

public Resource GetResource(int id)
{
    return User.GetUserRepository().FindResource(id);
}

扩展方法将是:

public static IUserRepository GetUserRepository(this IPrincipal principal)
{
    var resourceRepository = new ResourceRepository();
    bool isAdmin = principal.IsInRole("Admin");
    if (isAdmin)
    {
        return new AdminRespository(resourceRepository);
    }
    else
    {
       return new UserRepository(principal.Identity, resourceRepository);
    }
}

我不想使用 AuthorizeAttribute 进行每个资源身份验证的原因是不同的资源可能有不同的代码来检查所有权,很难将代码集中在一个属性中,并且它需要额外的数据库操作,而这并不是真正必要的。另一个问题是,AuthroizeAttribute 发生在参数绑定之前,因此您需要确保操作的参数来自路由数据。否则,例如,从帖子正文中,您将无法获取参数值。

于 2013-09-17T16:49:23.413 回答
2

您需要将您的授权外部化。您希望将整个授权逻辑移至单独的层或服务。

有几个框架 - 使用不同的语言 - 可以让你做到这一点。在 .NET 世界中,正如其他答案中所建议的那样,您拥有基于声明的授权。微软在这里有一篇很棒的文章。

我建议您采用标准化方法,即 XACML,即可扩展访问控制标记语言。XACML 为您提供 3 件事:

  • 具有策略决策点(PDP - 这是您的授权服务)概念的标准架构,可以服务于是/否决策
  • 一种标准语言,使用任意数量的参数/属性(包括用户属性和资源信息)来表达您的授权逻辑。
  • 将您的授权问题发送给 PDP 的请求/响应方案。

如果我们重新审视您的示例,您将有以下内容:

public Resource GetResource(int id)
{
     var resource = resourceRepository.Find(id);
    if (isAuthorized(User.Identity,resource))
    {
        throw new HttpResponseException(HttpStatusCode.Unauthorized);
    }

    return resource;
}

public bool isAuthorized(User u, Resource r){
   // Create XACML request here
   // Call out to PDP
   // return boolean decision
}

您的 PDP 将包含以下规则:

  • 当且仅当 resource.owner==user.id 时,用户才能对资源执行 action==view
  • 具有角色==管理员的用户可以对资源执行操作==查看。

XACML 的好处是您可以独立于您的代码扩展您的授权规则/逻辑。这意味着您不必在逻辑更改时触碰您的应用程序代码。XACML 还可以满足更多参数/属性——例如设备 ID、IP、一天中的时间……最后,XACML 并不特定于 .NET。它适用于许多不同的框架。

您可以在此处阅读 XACML ,也可以在我自己的博客上阅读有关授权的文章。维基百科也有一个关于这个主题的不错的页面。

于 2013-09-19T18:39:47.280 回答
2

我会考虑实现一个自定义System.Web.Http.AuthorizeAttribute,您可以将其应用于需要此特定授权规则的操作。在自定义授权中,如果用户是 Admins 组的成员,或者他们是资源的作者,您可以允许访问。

编辑:

根据 OP 的编辑,请允许我扩展我所说的内容。如果您覆盖 AuthorizeAttribute,您可以添加如下逻辑:

public class AuthorizeAdminsAndAuthors : System.Web.Http.AuthorizeAttribute
{
    protected override bool IsAuthorized(HttpActionContext actionContext)
    {
        return currentUser.IsInRole("Admins") || IsCurrentUserAuthorOfPost(actionContext);
    }

    private bool IsCurrentUserAuthorOfPost(HttpActionContext actionContext)
    {
        // Get id for resource from actionContext
        // look up if user is author of this post
        return true;
    }

这是伪代码,但应该传达这个想法。如果您有一个根据您的要求确定授权的 AuthorizeAttribute:当前请求来自帖子作者或管理员,那么您可以将 AuthorizeAdminsAndAuthors 属性应用于您需要此级别授权的任何资源。所以你的资源看起来像:

[AuthorizeAdminsAndAuthors]
public Resource GetResource(int id)
{
    var resource = resourceRepository.Find(id);
    return resource;
}
于 2013-09-17T17:07:51.993 回答
1

还可以查看基于声明的授权方法 - 包括从 .NET 4.5 开始。

http://leastprivilege.com/2012/10/26/using-claims-based-authorization-in-mvc-and-web-api/

于 2013-09-18T06:30:04.743 回答
1

很容易

要求

    public class PrivateProfileRequirement : IAuthorizationRequirement
    {
        public string ClaimType { get; }

        public PrivateProfileRequirement(string claimType)
        {
            ClaimType = claimType;
        }
    }

    public class PrivateProfileHandler : AuthorizationHandler<PrivateProfileRequirement>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PrivateProfileRequirement requirement)
        {
            if (context.User != null)
            {
                if (context.User.Claims.Any(c => string.Equals(c.Type, requirement.ClaimType, StringComparison.OrdinalIgnoreCase)))
                {
                    if (context.User.Identities.Any(i => string.Equals(i.GetId(), context.Resource)))
                    {
                        context.Succeed(requirement);
                    }
                }
            }

            return Task.CompletedTask;
        }
    }

启动.cs

        services.AddAuthorization(options =>
        {
            options.AddPolicy("PrivateProfileRequirement",
                policy => policy
                         .RequireAuthenticatedUser()
                         .RequireRole(Role.Profile.ToRole())
                         .AddRequirements(new PrivateProfileRequirement(ClaimTypes.NameIdentifier)));
        });

控制器

public class ProfileController : Controller
{
    private readonly IAuthorizationService _authorizationService;

    public ProfileController(IAuthorizationService authorizationService)
    {
        _authorizationService = authorizationService;
    }
}

行动

public async Task<IActionResult> OnGetAsync(int id)
{
    var profile = _profileRepository.Find(id);

    if (profile == null)
    {
        return new NotFoundResult();
    }

    var authorizationResult = await _authorizationService
            .AuthorizeAsync(User, profile.Id, "PrivateProfileRequirement");

    if (authorizationResult.Succeeded)
    {
        return View();
    }

   return new ChallengeResult();     
}
于 2018-06-02T07:04:00.147 回答