【问题标题】:Can I manually invoke the ClaimsAuthorizationManager for a given ActionResult?我可以为给定的 ActionResult 手动调用 ClaimsAuthorizationManager 吗?
【发布时间】:2013-12-28 00:32:14
【问题描述】:

我有一个 ASP.NET MVC Web 应用程序,它通过 WIF 和 Thinktecture.IdentityModel 使用基于声明的授权。但是,我不只是拦截未经授权的请求,而是想修剪我的导航菜单以仅显示当前用户可访问的链接。

我最初的想法是接受一个操作列表(ActionResult、ActionLink、路由值字典,还不太确定)并执行我的自定义ClaimsAuthorizationManger.CheckAccess 例程。为了做到这一点,我需要生成一个 AuthorizationContext,但是,我不确定创建上下文的框架实用程序(或者最好是抽象)是否可以访问。有谁知道这是否可能?或者,如果我说这一切都错了,你有什么建议?

谢谢,节日快乐!

【问题讨论】:

    标签: c# asp.net-mvc wif claims-based-identity thinktecture-ident-model


    【解决方案1】:

    正如 Dominick Baier 所指出的,我能够使用 Thinktecture.IdentityModel 创建一个不错的解决方案。 I've posted the code in a Gist。这个特定的功能似乎没有记录,但我能够将相关博客文章和文档中的点点滴滴拼凑起来。我会很感激任何反馈,因为我确信 IdentityModel 或 WIF 的某些角落可以减轻我的代码的影响。也就是说,这运行良好。让我们从一个示例用法开始:

    剃须刀

    // This would likely be a partial view with per-user output caching
    @Html.BuildNavigation(new List<NavigationItem>
    {
        new NavigationItem("Production", MVC.Production.Home.Index()),
        new NavigationItem("Inventory", MVC.Inventory.Home.Index()),
        new NavigationItem("Quality Control", MVC.QualityControl.Home.Index()),
        new NavigationItem("Customers", MVC.Sales.Home.Index()),
        new NavigationItem("Vendors", MVC.Vendors.Companies.Index()),
    })
    

    如下所示,BuildNavigation 帮助函数将呈现一个&lt;ul&gt;,其中包含用户有权查看的所有导航项。如果您想知道MVC.{Area}.{Controller}.{Action}() 语句,这些是T4MVC 助手。没有必要使用 T4MVC。为方便起见,NavigationItem 提供了一个接受ActionResult 的覆盖。实际上,NavigationItem 只包含一个要显示的string 和一个RouteValueDictionary (see the Gist)。

    HTML 帮助程序扩展

    为了利用 Thinktecture.IdentityModel 提供的现有授权实用程序,您必须创建一个RequestContext。请注意,我们从RouteValueDictionary 构建RouteData,然后为授权助手创建一个新的RequestContext重要的旁注:如果您像我一样在 MVC 项目中使用区域,AddNamespaceInfo 函数是最重要的。如果您在不同的区域中有重复的控制器名称,这就是如何控制器工厂将知道如何访问正确的控制器。否则,您将收到异常。

    public static class NavigationHelper
    {
        public static MvcHtmlString BuildNavigation(this HtmlHelper htmlHelper, IEnumerable<NavigationItem> navigationItems)
        {
            var container = new TagBuilder("ul");
            container.MergeAttribute("id", "menu");
            var innerHtmlBuilder = new StringBuilder();
            foreach (var item in navigationItems.Where(item => IsAuthorized(htmlHelper, item.RouteValueDictionary)))
            {
                innerHtmlBuilder.Append(
                    new TagBuilder("li")
                    {
                        InnerHtml = htmlHelper.ActionLink(
                            item.LinkText, 
                            item.RouteValueDictionary["action"] as string,
                            item.RouteValueDictionary["controller"] as string, 
                            item.RouteValueDictionary, null).ToHtmlString()
                    });
            }
            container.InnerHtml = innerHtmlBuilder.ToString();
            return new MvcHtmlString(container.ToString());
        }
    
        private static bool IsAuthorized(this HtmlHelper htmlHelper, RouteValueDictionary routeValues)
        {
            var routeData = BuildRouteData(htmlHelper.RouteCollection, routeValues);
            var context = BuildRequestContext(htmlHelper, routeData);
            return ClaimsAuthorizationHelper.CheckAccess(context);
        }
    
        private static RouteData BuildRouteData(IEnumerable<RouteBase> routeCollection, RouteValueDictionary routeValues)
        {
            object controllerValue;
            routeValues.TryGetValue("controller", out controllerValue);
            var controllerName = controllerValue as string;
    
            object actionValue;
            routeValues.TryGetValue("action", out actionValue);
            var actionName = actionValue as String;
    
            object areaValue;
            routeValues.TryGetValue("area", out areaValue);
            var areaName = areaValue as String ?? "";
    
            var routeData = new RouteData();
            routeData.Values.Add("action", actionName);
            routeData.Values.Add("controller", controllerName);
            routeData.Values.Add("area", areaName);
            AddNamespaceInfo(routeData, routeCollection, areaName, controllerName, actionName);
    
            return routeData;
        }
    
        private static RequestContext BuildRequestContext(this HtmlHelper htmlHelper, RouteData routeData)
        {
            var claimsPrincipal = htmlHelper.ViewContext.HttpContext.User as ClaimsPrincipal;
            var requestContext = new RequestContext(htmlHelper.ViewContext.HttpContext, routeData);
            requestContext.HttpContext.User = claimsPrincipal;
    
            return requestContext;
        }
    
        private static void AddNamespaceInfo(RouteData routeData, IEnumerable<RouteBase> routeCollection, string areaName, string controllerName, string actionName)
        {
            var route = routeCollection.GetRoute(areaName, controllerName, actionName);
    
            if (route != null)
            {
                routeData.DataTokens.Add("Namespaces", route.DataTokens["Namespaces"]);
            }
        }
    }
    

    Thinktecture.IdentityModel ClaimsAuthorizeAttribute Wrapper

    我遇到的另一个绊脚石是ClaimsAuthorizeAttribute 的封闭性。这是我怀疑通过更深入地了解 WIF 可能会消除的一个领域。但是,当时,我已经围绕 ClaimsAuthorizeAttribute 创建了一个包装器,它允许我将属性转换为声明。

    public class ClaimsAuthorizeAttribute : Thinktecture.IdentityModel.Authorization.Mvc.ClaimsAuthorizeAttribute
    {
        private readonly string _action;
        private readonly string[] _resources;
    
        public ClaimsAuthorizeAttribute(string action, params string[] resources)
            :base(action, resources)
        {
            _action = action;
            _resources = resources;
        }
    
        public IEnumerable<Claim> GetClaims()
        {
            return _resources.Select(r => new Claim(_action, r));
        }
    }
    

    索赔授权助手

    最后,这里是 ClaimsAuthorizationHelper,它负责解析必要的控制器和操作方法,检索资源声明,并调用 Thinktecture IdentityModel 提供的 ClaimsAuthorization 实用程序。

    public static class ClaimsAuthorizationHelper
    {
        public static bool CheckAccess(RequestContext requestContext)
        {
            var routeData = requestContext.RouteData;
            var controllerName = routeData.Values["controller"] as string;
            var actionName = routeData.Values["action"] as string;
    
            var controller = GetControllerByName(requestContext, controllerName);
            var controllerDescriptor = new ReflectedControllerDescriptor(controller.GetType());
            var controllerContext = new ControllerContext(requestContext, controller);
            var actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName);
    
            var resourceClaims = actionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof (ClaimsAuthorizeAttribute), false)
                .Cast<ClaimsAuthorizeAttribute>()
                .SelectMany(auth => auth.GetClaims()).ToList();
    
            resourceClaims.AddRange(actionDescriptor.GetCustomAttributes(typeof(ClaimsAuthorizeAttribute), false).Cast<ClaimsAuthorizeAttribute>()
                .SelectMany(c => c.GetClaims()));
    
            var hasAccess = ClaimsAuthorization.CheckAccess(actionName, resourceClaims.ToArray());
            return hasAccess;
        }
    
        public static ControllerBase GetControllerByName(RequestContext requestContext, string controllerName)
        {
            var factory = ControllerBuilder.Current.GetControllerFactory();
            var controller = factory.CreateController(requestContext, controllerName);
            if (controller == null)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "The current controller factory, \"{0}\", did not return a controller for the name \"{1}\".", factory.GetType(), controllerName));
            }
    
            return (ControllerBase)controller;
        }
    }
    

    其他代码

    为了简洁起见,省略了其他一些帮助器和类。请see the Gist for the full code

    【讨论】:

      【解决方案2】:

      在我们的系统中,我们在用户数据库中定义了自定义声明。自定义声明定义用户有权访问的菜单项。当用户登录(使用 Thinktecture Identity Server)时,这些声明将作为附加声明添加到用户令牌中。

      当我们的应用程序显示用户的菜单时,它会从当前主体获取声明并迭代该列表以查找菜单声明,并仅为这些声明创建链接。

      例如,用户可能有权添加新交易和查看现有交易,但无权修改或删除。所以他的主张是:

      name = "http://schemas.mycompany.com/2013/10/identity/claims/newTransaction" value = "true"
      name = "http://schemas.mycompany.com/2013/10/identity/claims/viewTransaction" value = "true"
      

      检查声明:

      var cp = (ClaimsPrincipal)Thread.CurrentPrincipal;
      
      if (cp.Claims.Contains(ClaimName))
      {
          // enable that function
      }
      

      请注意,我们的权限是为程序功能命名的,而不是 MVC 操作或链接。这使我们可以灵活地在代码中重命名操作,而无需更改声明本身。

      这似乎是使用基于声明的授权的推荐方式。至少,这是我在 MSDN 示例和Programming Windows Identity Foundation 一书中看到的。

      【讨论】:

        【解决方案3】:

        你可以自己创建一个AuthorizationContext,你可以通过FederatedAuthentication类调用注册的授权管理器。

        Thinktecture.IdentityModel 还具有一个名为 ClaimsAuthorization 的静态类,可帮助完成该过程。

        【讨论】:

        • 感谢 Dominick,我发布了一个解决方案。我非常感谢您对此的反馈。有可能不必要地重复了一些框架和/或 IdentityModel 代码,但这只是因为我找不到我要找的东西:)
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-05-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多