ASP MVC Workflow工具构成逻辑和权限

Rap*_*ion 6 c# asp.net-mvc windows-authentication razor

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

一个角色表示他们的资历(访客,用户,高级用户,经理等),另一个角色表示他们的角色/部门(分析,开发,测试等).Analytics中的用户可以创建一个请求,然后将链向上流到Development等等:

楷模

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; }
    ...
}
Run Code Online (Sandbox Code Playgroud)

在控制器中,我有一个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);
            ...         
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  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);
    }
}
Run Code Online (Sandbox Code Playgroud)

CreateHistory视图本身将根据状态呈现不同的部分形式.我的意图是我可以为进程中的每个阶段使用单个通用的CreateHistory方法,使用Status作为参考来确定要呈现的部分View.

现在,问题在于渲染和限制视图中的可用操作.我的CreateHistory View使用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>
Run Code Online (Sandbox Code Playgroud)

在正确的时间出现正确的动作是很容易的部分,但感觉就像一个笨拙的方法,我不知道如何以这种方式管理权限.所以我的问题是:

我是否应该在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)
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

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

对不起,很长的帖子,任何帮助将不胜感激.

小智 1

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

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

以下是文章的片段,以防文章被删除。:)

这是检查控制器的授权属性的扩展方法。在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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

这是一个获取 ControllerBase 对象的辅助方法,该对象在上面的代码片段中用于询问操作过滤器。

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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

这是自定义 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;
  }
}
Run Code Online (Sandbox Code Playgroud)

像通常调用 ActionLink 一样调用它

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