我有一个ASP.NET MVC Web应用程序,它通过WIF和Thinktecture.IdentityModel
使用基于声明的授权。但是,我不想仅仅截取未经授权的请求,而是想修剪我的导航菜单,只显示当前用户可以访问的链接。
我最初的想法是接受一系列操作(ActionResult,ActionLink,路由值字典,还不太确定)并执行我的自定义ClaimsAuthorizationManger.CheckAccess
例程。为了做到这一点,我需要生成一个AuthorizationContext,但是,我不确定创建上下文的框架实用程序(或最好是抽象)是否可访问。有人知道这可能吗?或者,如果我说这一切都错了,你有什么建议?
谢谢,节日快乐!
答案 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
只包含要显示的string
和RouteValueDictionary
(see 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的静态类,可以帮助完成该过程。