1

I have an ASP.NET MVC web app which is utilizing claims-based authorization via WIF and Thinktecture.IdentityModel. However, rather than just intercepting the unauthorized request, I'd like to trim my navigation menus to only display the links accessible to the current user.

My initial thoughts are to accept a list of actions (ActionResult, ActionLink, route value dictionary, not quite sure yet) and execute my custom ClaimsAuthorizationManger.CheckAccess routine. In order to do this, I need to generate an AuthorizationContext, however, I'm not sure if the framework utilities (or preferably abstractions) creating the context are accessible. Anyone know if this possible? Or, if I'm going about this all wrong, what do you suggest?

Thanks and happy holidays!

4

3 回答 3

4

正如 Dominick Baier 所指出的,我能够使用Thinktecture.IdentityModel. 我已经在 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提供了一个接受 an 的覆盖ActionResult。实际上,NavigationItem仅由string要显示的 a 和 a RouteValueDictionary参见要点)组成。

HTML 助手扩展

为了利用 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负责解析必要的控制器和操作方法,检索资源声明,并调用ClaimsAuthorizationThinktecture IdentityModel 提供的实用程序。

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

其他代码

为简洁起见,还有一些其他的助手和类被省略了。请参阅 Gist 以获取完整代码

于 2013-12-27T16:32:14.323 回答
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 示例和Programming Windows Identity Foundation 一书中看到的。

于 2013-12-24T17:32:49.843 回答
2

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

Thinktecture.IdentityModel 也有一个名为 ClaimsAuthorization 的静态类,它有助于该过程。

于 2013-12-27T10:42:21.770 回答