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

时间:2017-03-09 10:53:42

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

我正在创建一个将在我们公司内部网上使用的工作流工具。用户通过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. 开发人员流程请求[开发,用户]
  4. 目前我有一个处理流程每个阶段的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 View本身将根据Status呈现不同的部分表单。我的意图是我可以为流程中的每个阶段使用单个通用的CreateHistory方法,使用Status作为参考来确定要呈现的部分View。

    现在,问题在于渲染和限制视图中的可用操作。我的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)
    {
        ...
    }
    

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

    对于这篇长篇文章感到抱歉,非常感谢任何帮助。

5 个答案:

答案 0 :(得分:1)

在使用MVC(或者,任何语言)进行编码时,我会尝试将所有或大部分逻辑语句保留在我的视图之外。

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

public bool IsAccessibleToManager { get; set; }

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

然后在Controller中填充它,并且可以在您认为合适时设置,可能在角色逻辑类中设置,将所有这些保存在一个位置。

对于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.
   }
}

答案 1 :(得分:1)

如何为每种类型的角色创建不同的视图,然后从单个操作返回相应的视图?

test.flatMap(x => {
  if (moveToP(x._2)) List(x._2) else List.empty
})

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

答案 2 :(得分:1)

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

首先,我要定义reloadData()枚举,而不是描述您的用户可能会采取的操作。也许你已经有了。

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

IAvailableActionFactory

在此内部,此提供程序将使用您当前在视图中添加的类似逻辑。这种方法将使视图保持清洁并确保逻辑的可测试性。在提供者内部,您可以为不同的用户使用不同的策略,使实现更加分离。

然后在控制器中定义对此提供程序的依赖关系,如果您还没有使用容器,则直接通过instantioate容器解析它。

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.
    }
}

然后在您的操作使用提供程序中获取可用操作,您可以创建新视图模型而不是包含操作,或者只是将其放到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); } 中的项目生成操作列表。

我希望它有所帮助。如果您对此有疑问,请与我联系。

答案 3 :(得分:1)

首先,如果您在基于布尔的操作中封装了大量逻辑,我强烈建议您使用规范模式thisthis。它具有高度可重用性,并且在现有逻辑更改或需要添加新逻辑时可以实现出色的可维护性。研究制作复合规范,准确指定满足的内容,例如:如果用户是经理并且请求未获批准。

现在关于你的观点中的问题 - 尽管我在过去遇到同样的问题时,我采用了类似ChrisDixon的方法。它简单易用,但回顾应用程序现在我发现它很乏味,因为它被if语句所掩盖。我现在采用的方法是创建自定义操作链接或自定义控件,在可能的情况下将授权转换为上下文。我开始编写一些代码来做到这一点,但最后意识到这必须是一个常见的问题,因此found something a lot better比我自己打算为这个答案写的。虽然针对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;
        }
    }

这是获取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;
        }
    }

这是自定义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)

答案 4 :(得分:0)

我建议与角色同时使用声明。如果角色需要访问资源,我将向他们提供对资源的声明,这意味着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