首页 > 解决方案 > 如何设计发布请求的多角色授权?

问题描述

在我正在构建的系统中,有一个复杂且不断变化的基于资源的授权。目前一共有六个角色。

该系统正在处理成员,所有成员都可以在他们自己的个人资料上编辑基本信息,另一个角色的另一个人可以在他们的个人资料上编辑更多信息等等。

我不知道哪种方法是用端点/操作来设计这个的最佳方法,比如编辑成员操作。我最终做了但不喜欢的是每个角色都有一个控制器操作、视图和视图模型。这样做而不是拥有一个视图模型的主要原因是,我觉得拥有某人甚至无法编辑的所有属性是没有意义的,那是过度发布,对吗?

我对结果不太满意。6 个视图模型、6 个视图、6 个非常相似的控制器操作、6 个验证器等。

我现在的想法是,当映射回域对象、视图和验证器类时,我将只有一个编辑操作,然后有一堆 if 语句。过度发布仍然存在,但使用 if 语句进行管理。我也是这么想的——如果系统变成API怎么办?api/members/1/edit/api/members/1/editAsTreasurer

你怎么看?有人有我没有想到的另一种解决方案吗?

一些代码部分,重复代码示例,当然在验证器类、视图和映射中还有更多,不确定要包含多少:

[HttpPost]
public IActionResult EditAsSecretary(EditMemberAsSecretaryViewModel viewModel)
{
    if (!ModelState.IsValid)
    {
        viewModel.Init(_basicDataProvider, _authorizationProvider.GetAuthorizedLogesForManageMember());
        return View("EditAsSecretary", viewModel);
    }

    var member = _unitOfWork.Members.GetByMemberNumber(viewModel.MemberNumber, true);
    if (member == null) return NotFound();

    // Authorize
    if (!_authorizationProvider.Authorize(viewModel.MemberInfo.LogeId, AdminType.Sekreterare))
        return Forbid();

    var user = _unitOfWork.Members.GetByUserName(User.Identity.Name);

    var finallyEmail = viewModel.MemberContactInfo.Email != null && member.Email == null &&
                       !member.HasBeenSentResetPasswordMail && member.MemberNumber != user.MemberNumber;

    _domainLogger.UpdateLog(viewModel, member, user);
    UpdateMember(viewModel, member, user.Id);
    _unitOfWork.Complete();

    if (finallyEmail) SendUserResetPasswordMail(member).Wait();

    TempData["Message"] = "Member has been updated.";

    return RedirectToAction("Details", "Members", new { memberNumber = member.MemberNumber });
}


[HttpPost]
public IActionResult EditAsManager(EditMemberAsManagerViewModel viewModel)
{
    if (!ModelState.IsValid)
    {
        viewModel.Init(_basicDataProvider, _authorizationProvider.GetAuthorizedLogesForManageMember());
        return View("EditAsManager", viewModel);
    }

    var member = _unitOfWork.Members.GetByMemberNumber(viewModel.MemberNumber, true);
    if (member == null) return NotFound();

    // Authorize
    if (!_authorizationProvider.Authorize(member.LogeId, AdminType.Manager))
        return Forbid();

    var user = _unitOfWork.Members.GetByUserName(User.Identity.Name);

    var finallyEmail = viewModel.MemberContactInfo.Email != null && member.Email == null &&
                       !member.HasBeenSentResetPasswordMail && member.MemberNumber != user.MemberNumber;

    _domainLogger.UpdateLog(viewModel, member, user);
    UpdateMember(viewModel, member, user.Id);
    _unitOfWork.Complete();

    if (finallyEmail) SendUserResetPasswordMail(member).Wait();

    TempData["Message"] = "Member has been updated.";

    return RedirectToAction("Details", "Members", new { memberNumber = member.MemberNumber });
}


private void UpdateMember(EditMemberAsSecretaryViewModel viewModel, Member member, string userId)
{
    _mapper.Map(viewModel, member);
    MapGodfathers(viewModel.MemberInfo, member);
    AfterUpdateMember(member, userId);
    _userManager.UpdateNormalizedEmailAsync(member).Wait();
}

private void UpdateMember(EditMemberAsManagerViewModel viewModel, Member member, string userId)
{
    _mapper.Map(viewModel, member);
    MapGodfathers(viewModel.MemberInfo, member);
    AfterUpdateMember(member, userId);
    _userManager.UpdateNormalizedEmailAsync(member).Wait();
}

标签: c#asp.net-core-mvcauthorization

解决方案


我现在的想法是,当映射回域对象、视图和验证器类时,我将只有一个编辑操作,然后有一堆 if 语句。过度发布仍然存在,但使用 if 语句进行管理

不。

除了使代码的可读性大大降低之外,它还带来了安全风险。每个动作都应该尽可能少地使用它需要的参数。拥有更多操作不会花费您任何费用,因此没有理由这样做。

尽管您的代码存在一些问题,但这有助于重复:

  1. 您似乎正在对从用户那里收到的内容进行安全验证,而不是使用当前经过身份验证的用户。这是一个大问题,因为您信任来自用户的数据。
    取而代之的是,创建一个自定义授权策略,使用您的业务逻辑检查用户类型。然后可以将它们添加到内置容器中,您可以使用:

    [Authorize(Policy = "EnsureManager")]
    public IActionResult EditAsManager(...)
    

    这将允许您删除所有重复的代码并更接近 SRP。

  2. 你的重复UpdateMember看起来你的模型是不相关的。在这种情况下,最好有一个基本模型,然后是具有所需属性的子模型:

    public abstract class EditMemberBaseViewModel
    {
        [Required]
        public Something Something { get; set; }
    }
    
    public class EditMemberAsSecretaryViewModel : EditMemberBaseViewModel
    {
        [Required]
        public AnotherThing AnotherThing { get; set; }
    }
    

    这将允许你有一个单一UpdateMember的,因为逻辑是基于EditMemberBaseViewModel而不是他们的孩子,据你所表明的是:

    private void UpdateMember(EditMemberAsManagerViewModel viewModel, Member member, string userId)
    {
        _mapper.Map(viewModel, member);
        MapGodfathers(viewModel.MemberInfo, member);
        AfterUpdateMember(member, userId);
        _userManager.UpdateNormalizedEmailAsync(member).Wait();
    }
    

最后一点,可能也是最重要的一点,这段代码有一个问题:

_userManager.UpdateNormalizedEmailAsync(member).Wait();

真的很糟糕。您正在让 ASP.NET Core 挂起一个等待该操作完成的整个线程。这是同步的,2000 年代的代码。
您需要学习在应用程序中为每个与 IO 相关的操作(如数据库调用)使用异步代码,否则性能会受到很大影响。举个例子:

public async Task<IActionResult> EditAsManager(...)
{
    .....
    await UpdateMemberAsync(...);
}

public async Task UpdateMemberAsync(...)
{
    await _userManager.UpdateNormalizedEmailAsync(member);
}

推荐阅读