我可以为给定的ActionResult手动调用ClaimsAuthorizationManager吗?

时间:2013-12-24 16:36:24

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

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

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

谢谢,节日快乐!

3 个答案:

答案 0 :(得分:4)

正如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辅助函数将呈现<ul>,其中包含用户有权查看的所有导航项。如果你想知道MVC.{Area}.{Controller}.{Action}()语句,那些是T4MVC帮助者。没有必要使用T4MVC。为方便起见,NavigationItem提供了覆盖ActionResult的覆盖。实际上,NavigationItem只包含要显示的stringRouteValueDictionarysee the Gist)。

HTML Helper Extensions

为了利用Thinktecture.IdentityModel提供的现有授权实用程序,您必须创建RequestContext。请注意,我们从RouteData构建RouteValueDictionary,然后为授权助手创建新的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

答案 1 :(得分:3)

在我们的系统中,我们在用户数据库中定义了自定义声明。自定义声明定义用户有权访问的菜单项。当用户登录(使用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示例和编程Windows Identity Foundation一书中看到的。

答案 2 :(得分:2)

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

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