【问题标题】:ASP MVC Workflow tool form logic and permissionsASP MVC Workflow 工具表单逻辑和权限
【发布时间】:2017-03-09 10:53:42
【问题描述】:

我正在创建一个将在我们公司的 Intranet 上使用的工作流工具。用户使用 Windows 身份验证进行身份验证,我设置了一个自定义 RoleProvider,将每个用户映射到一对角色。

一个角色表示他们的资历(访客、用户、高级用户、经理等),另一个角色表示他们的角色/部门(分析、开发、测试等)。 Analytics 中的用户能够创建一个请求,然后沿着链向上流动到开发等:

模型

public class Request
{
    public int ID { get; set; }
    ...
    public virtual ICollection<History> History { get; set; }
    ...
}

public class History
{
    public int ID { get; set; }
    ...
    public virtual Request Request { get; set; }
    public Status Status { get; set; }
    ...
}

在控制器中,我有一个 Create() 方法,它将创建 Request 标头记录和第一个 History 项:

请求控制器

public class RequestController : BaseController
{
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create (RequestViewModel rvm)
    {
        Request request = rvm.Request
        if(ModelState.IsValid)
        {
            ...
            History history = new History { Request = request, Status = Status.RequestCreated, ... };
            db.RequestHistories.Add(history);
            db.Requests.Add(request);
            ...         
        }
    }
}

请求的每个进一步阶段都需要由链中的不同用户处理。该过程的一小部分是:

  1. 用户创建请求 [分析,用户]
  2. 经理授权请求 [分析,经理]
  3. 开发人员处理请求 [开发,用户]

目前我有一个 CreateHistory() 方法来处理流程的每个阶段。从视图中拉出新历史项目的状态:

// GET: Requests/CreateHistory
public ActionResult CreateHistory(Status status)
{
    History history = new History();
    history.Status = status;
    return View(history);
}

// POST: Requests/CreateHistory
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateHistory(int id, History history)
{
    if(ModelState.IsValid)
    {
        history.Request = db.Requests.Find(id);
        ...
        db.RequestHistories.Add(history);
    }
}

CreateHistory 视图本身将根据状态呈现不同的部分表单。我的意图是我可以为流程中的每个阶段使用一个通用的 CreateHistory 方法,使用 Status 作为参考来确定要渲染哪个部分视图。

现在,问题在于呈现和限制视图中的可用操作。我的 CreateHistory 视图变得臃肿,If 语句根据请求的当前状态确定操作的可用性:

@* Available user actions *@
<ul class="dropdown-menu" role="menu">
    @* Analyst has option to withdraw a request *@
    <li>@Html.ActionLink("Withdraw", "CreateHistory", new { id = Model.Change.ID, status = Status.Withdrawn }, null)</li>

    @* Request manager approval if not already received *@
    <li>...</li>

    @* If user is in Development and the Request is authorised by Analytics Manager *@
    <li>...</li>        
    ...
</ul>

在正确的时间出现正确的操作是一件容易的事,但感觉是一种笨拙的方法,我不确定如何以这种方式管理权限。所以我的问题是:

我是否应该在 RequestController 中为流程的每个阶段创建一个单独的方法,即使这会导致很多非常相似的方法?

一个例子是:

public ActionResult RequestApproval(int id)
{
    ...
}
[MyAuthoriseAttribute(Roles = "Analytics, User")]
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult RequestApproval(int id, History history)
{
    ...
}

public ActionResult Approve (int id)
{
    ...
}
[MyAuthoriseAttribute(Roles = "Analytics, Manager")]
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Approve (int id, History history)
{
    ...
}

如果是这样,我该如何处理在视图中呈现适当的按钮?我只希望一组有效的操作显示为控件。

抱歉发了这么长的帖子,任何帮助将不胜感激。

【问题讨论】:

  • 使用具有 bool CanWithdraw 等属性的视图模型(这些属性的值是根据当前用户设置的,并且状态会让您查看更清晰/更具可读性,尽管您仍然需要if 状态)

标签: c# asp.net-mvc razor windows-authentication


【解决方案1】:

首先,如果您有很多逻辑封装在基于布尔的操作中,我强烈建议您使用规范模式this 和this 应该会让您有良好的开端。当现有逻辑发生更改或您需要添加新逻辑时,它具有高度可重用性并具有很好的可维护性。研究制定复合规范,准确指定可以满足的内容,例如如果用户是经理并且请求未获批准。

现在关于您认为的问题 - 尽管过去我遇到同样的问题时,我采用了与 ChrisDixon 类似的方法。它简单易用,但现在回顾应用程序,我发现它很乏味,因为它被隐藏在 if 语句中。我现在采取的方法是创建自定义操作链接或自定义控件,尽可能将授权纳入上下文。我开始编写一些代码来执行此操作,但最终意识到这一定是一个常见问题,因此found something a lot better 比我自己打算为这个答案写的要多。虽然针对 MVC3,但逻辑和目的应该仍然成立。

如果文章被删除,下面是 sn-ps。 :)

这是检查控制器的授权属性的扩展方法。在foreach 循环中,您可以检查您自己的自定义属性是否存在并对其进行授权。

public static class ActionExtensions
    {
        public static bool ActionAuthorized(this HtmlHelper htmlHelper, string actionName, string controllerName)
        {
            ControllerBase controllerBase = string.IsNullOrEmpty(controllerName) ? htmlHelper.ViewContext.Controller : htmlHelper.GetControllerByName(controllerName);
            ControllerContext controllerContext = new ControllerContext(htmlHelper.ViewContext.RequestContext, controllerBase);
            ControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor(controllerContext.Controller.GetType());
            ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName);

            if (actionDescriptor == null)
                return false;

            FilterInfo filters = new FilterInfo(FilterProviders.Providers.GetFilters(controllerContext, actionDescriptor));

            AuthorizationContext authorizationContext = new AuthorizationContext(controllerContext, actionDescriptor);
            foreach (IAuthorizationFilter authorizationFilter in filters.AuthorizationFilters)
            {
                authorizationFilter.OnAuthorization(authorizationContext);
                if (authorizationContext.Result != null)
                    return false;
            }
            return true;
        }
    }

这是一个获取ControllerBase对象的辅助方法,在上面的sn-p中用于询问动作过滤器。

internal static class Helpers
    {
        public static ControllerBase GetControllerByName(this HtmlHelper htmlHelper, string controllerName)
        {
            IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
            IController controller = factory.CreateController(htmlHelper.ViewContext.RequestContext, controllerName);
            if (controller == null)
            {
                throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "The IControllerFactory '{0}' did not return a controller for the name '{1}'.", factory.GetType(), controllerName));
            }
            return (ControllerBase)controller;
        }
    }

这是自定义 Html Helper,如果授权通过,则生成操作链接。如果未经授权,我已从原始文章中对其进行了调整以删除链接。

public static MvcHtmlString ActionLinkAuthorized(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
{
  if (htmlHelper.ActionAuthorized(actionName, controllerName))
  {
    return htmlHelper.ActionLink(linkText, actionName, controllerName, routeValues, htmlAttributes);
  }
  else
  {
    return MvcHtmlString.Empty;
  }
}

像通常调用 ActionLink 一样调用它

@Html.ActionLinkAuthorized("Withdraw", "CreateHistory", new { id = Model.Change.ID, status = Status.Withdrawn }, null)

【讨论】:

    【解决方案2】:

    在使用 MVC(或者,任何语言)编码时,我会尽量让所有或大部分逻辑语句远离我的视图。

    我会将您的逻辑处理保留在您的 ViewModel 中,所以:

    public bool IsAccessibleToManager { get; set; }
    

    那么,在您看来,使用 @if(Model.IsAccessibleToManager) {} 这样的变量很简单。

    然后将其填充到您的控制器中,并且可以按照您认为合适的方式进行设置,可能在一个角色逻辑类中,将所有这些都保存在一个地方。

    对于Controller中的方法,保持相同的方法,并在方法本身内部进行逻辑处理。这完全取决于您的结构和数据存储库,但我会将尽可能多的逻辑处理本身保留在存储库级别,因此在您获取/设置数据的每个地方都是相同的。

    通常,您的属性标签不允许某些角色使用这些方法,但在您的场景中,您可以这样做...

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Approve (int id, History history)
    {
        try {
        // The logic processing will be done inside ApproveRecord and match up against Analytics or Manager roles.
        _historyRepository.ApproveRecord(history, Roles.GetRolesForUser(yourUser)); 
       } 
       catch(Exception ex) {
           // Could make your own Exceptions here for the user not being authorised for the action.
       }
    }
    

    【讨论】:

      【解决方案3】:

      如何为每种类型的角色创建不同的视图,然后通过单个操作返回适当的视图?

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Approve (int id, History history)
      {
          // Some pseudo-logic here:
          switch(roles)
          {
              case Manager:
              case User:
              {
                 return View("ManagerUser");
              }
              case Manager:
              case Analyst:
              {
                 return View("ManagerAnalyst");
              }
          }
      }
      

      当然,这种方法需要您为不同的角色组合创建一个视图,但至少您能够呈现适当的视图代码,而不会使 UI 逻辑使视图混乱。

      【讨论】:

        【解决方案4】:

        我建议您使用提供程序为用户生成可用操作的列表。

        首先我会定义AwailableAction 枚举,而不是描述您的用户可能执行的操作。也许你已经有了。

        然后你可以定义IAvailableActionFactory接口并用你的逻辑实现它:

        public interface IAvailableActionProvider 
        {
            ReadOnlyCollection<AwailableAction> GetAvailableActions(User, Request, History/*, etc*/) // Provide parameters that need to define actions.
        }
        
        public class AvailableActionProvider : IAvailableActionProvider 
        {
            ReadOnlyCollection<AwailableAction> GetAvailableActions(User, Request, History)
            {
                // You logic goes here.
            }
        }
        

        在内部,此提供程序将使用您当前在视图中实现的类似逻辑。这种方法将保持视图干净并确保逻辑的可测试性。可选地,在 provivder 内部,您可以为不同的用户使用不同的策略,并使实现更加解耦。

        然后在控制器中定义对这个提供者的依赖,如果你还没有使用容器,可以直接通过实例化容器解决它。

        public class RequestController : BaseController
        {
            private readonly IAvailableActionProvider _actionProvider;
        
            public RequestController(IAvailableActionProvider actionProvider)
            {
                _actionProvider = actionProvider;
            }
        
            public RequestController() : this(new AvailableActionProvider())
            {
            }
        
            ...
        }
        

        然后在您的操作中使用提供程序来获取可用操作,您可以创建新的视图模型而不是包含操作或简单地将其放入ViewBag:

        // GET: Requests/CreateHistory
        public ActionResult CreateHistory(Status status)
        {
            History history = new History();
            history.Status = status;
        
            ViewBag.AvailableActions = _actionProvider.GetAvailableActions(User, Request, history);
        
            return View(history);
        }
        

        最后,您可以根据ViewBag 中的项目生成操作列表。

        我希望它有所帮助。如果您对此有任何疑问,请告诉我。

        【讨论】:

          【解决方案5】:

          我建议同时使用声明和角色。如果角色需要访问资源,我会给他们一个资源声明,即 actionResult。如果他们的角色与控制器匹配,为了简单起见,我目前检查他们是否拥有资源的所有权。我在控制器级别使用角色,因此如果来宾或其他帐户需要匿名访问,我可以简单地添加属性,但通常我应该将它放在正确的控制器中。

          这里有一些代码要显示。

          <Authorize(Roles:="Administrator, Guest")>
          Public Class GuestController
          Inherits Controller
              <ClaimsAuthorize("GuestClaim")>
                      Public Function GetCustomers() As ActionResult
                          Dim guestClaim As Integer = UserManager.GetClaims(User.Identity.GetUserId()).Where(Function(f) f.Type = "GuestClaim").Select(Function(t) t.Value).FirstOrDefault()
          
                          Dim list = _customerService.GetCustomers(guestClaim)
          
                          Return Json(list, JsonRequestBehavior.AllowGet)
                      End Function
          
          End Class
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2013-09-23
            • 1970-01-01
            • 1970-01-01
            • 2014-11-13
            • 1970-01-01
            • 2010-09-27
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多