ASP.NET MVC:如何在 C# 中使用反射查找具有 [Authorize] 属性的控制器? (或如何构建动态站点.主菜单?)

发布于 2024-09-04 14:32:00 字数 2554 浏览 5 评论 0原文

也许我应该在深入讨论标题问题之前备份并扩大范围...

我目前正在 ASP.NET MVC 1.0 中编写一个 Web 应用程序(尽管我的 PC 上确实安装了 MVC 2.0,所以我没有完全限制为 1.0)——我从标准 MVC 项目开始,该项目具有基本的“欢迎使用 ASP.NET MVC”,并在右上角显示 [Home] 选项卡和 [About] 选项卡。相当标准,对吧?

我添加了 4 个新的控制器类,我们称它们为“天文学家”、“生物学家”、“化学家”和“物理学家”。每个新控制器类都附加了 [Authorize] 属性。

例如,对于 BiographerController.cs,

[Authorize(Roles = "Biologist,Admin")]
public class BiologistController : Controller
{ 
    public ActionResult Index() { return View(); }
}

这些 [Authorize] 标签自然会根据角色限制哪些用户可以访问不同的控制器,但我想根据角色在 Site.Master Page 中的网站顶部动态构建一个菜单用户是.的一部分.例如,如果“JoeUser”是角色“天文学家”和“物理学家”的成员,则导航菜单会显示:

[主页] [天文学家] [物理学家] [关于]

当然,它不会列出指向“生物学家”或“化学家”控制器索引页面的链接。

或者,如果“JohnAdmin”是角色“Admin”的成员,则指向所有 4 个控制器的链接将显示在导航栏中。

好吧,你大概明白了...现在真正的问题...


这个关于在 ASP.NET 中构建动态菜单的 StackOverflow 主题的答案,我试图了解我如何将全面落实这一举措。 (我是一个新手,需要更多指导,所以请直接告诉我。)

答案建议扩展 Controller 类(将其称为“ExtController”),然后让每个新的WhateverController 继承自 ExtController。

我的结论是,我需要在此 ExtController 构造函数中使用反射来确定哪些类和方法附加了 [Authorize] 属性来确定角色。然后使用静态字典,将角色和控制器/方法存储在键值对中。

我想象它是这样的:

public class ExtController : Controller
{
    protected static Dictionary<Type,List<string>> ControllerRolesDictionary;

    protected override void OnActionExecuted(ActionExecutedContext filterContext)   
    {   
        // build list of menu items based on user's permissions, and add it to ViewData  
        IEnumerable<MenuItem> menu = BuildMenu();  
        ViewData["Menu"] = menu;
    }

    private IEnumerable<MenuItem> BuildMenu()
    {
        // Code to build a menu
        SomeRoleProvider rp = new SomeRoleProvider();
        foreach (var role in rp.GetRolesForUser(HttpContext.User.Identity.Name))
        {

        }
    }

    public ExtController()
    {
        // Use this.GetType() to determine if this Controller is already in the Dictionary
        if (!ControllerRolesDictionary.ContainsKey(this.GetType()))
        {
            // If not, use Reflection to add List of Roles to Dictionary 
            // associating with Controller
        }
    }
}

这可行吗?如果是这样,我如何在 ExtController 构造函数中执行反射来发现 [Authorize] 属性和相关角色(如果有)

!请随意超出此问题的范围,并提出解决此“基于角色的动态站点.主菜单”问题的替代方法。我首先承认这可能不是最好的方法。

编辑

经过大量阅读和实验,我想出了自己的解决方案。请参阅下面的我的答案。欢迎任何建设性的反馈/批评!

Maybe I should back-up and widen the scope before diving into the title question...

I'm currently writing a web app in ASP.NET MVC 1.0 (although I do have MVC 2.0 installed on my PC, so I'm not exactly restricted to 1.0) -- I've started with the standard MVC project which has your basic "Welcome to ASP.NET MVC" and shows both the [Home] tab and [About] tab in the upper-right corner. Pretty standard, right?

I've added 4 new Controller classes, let's call them "Astronomer", "Biologist", "Chemist", and "Physicist". Attached to each new controller class is the [Authorize] attribute.

For example, for the BiologistController.cs

[Authorize(Roles = "Biologist,Admin")]
public class BiologistController : Controller
{ 
    public ActionResult Index() { return View(); }
}

These [Authorize] tags naturally limit which user can access different controllers depending on Roles, but I want to dynamically build a Menu at the top of my website in the Site.Master Page based on the Roles the user is a part of. So for example, if "JoeUser" was a member of Roles "Astronomer" and "Physicist", the navigation menu would say:

[Home] [Astronomer] [Physicist]
[About]

And naturally, it would not list links to "Biologist" or "Chemist" controller Index page.

Or if "JohnAdmin" was a member of Role "Admin", links to all 4 controllers would show up in the navigation bar.

Ok, you prolly get the idea... Now for the real question...


Starting with the answer from this StackOverflow topic about Dynamic Menu building in ASP.NET, I'm trying to understand how I would fully implement this. (I'm a newbie and need a little more guidance, so please bare with me.)

The answer proposes Extending the Controller class (call it "ExtController") and then have each new WhateverController inherit from ExtController.

My conclusion is that I would need to use Reflection in this ExtController Constructor to determine which Classes and Methods have [Authorize] attributes attached to them to determine the Roles. Then using a Static Dictionary, store the Roles and Controllers/Methods in key-value pairs.

I imagine it something like this:

public class ExtController : Controller
{
    protected static Dictionary<Type,List<string>> ControllerRolesDictionary;

    protected override void OnActionExecuted(ActionExecutedContext filterContext)   
    {   
        // build list of menu items based on user's permissions, and add it to ViewData  
        IEnumerable<MenuItem> menu = BuildMenu();  
        ViewData["Menu"] = menu;
    }

    private IEnumerable<MenuItem> BuildMenu()
    {
        // Code to build a menu
        SomeRoleProvider rp = new SomeRoleProvider();
        foreach (var role in rp.GetRolesForUser(HttpContext.User.Identity.Name))
        {

        }
    }

    public ExtController()
    {
        // Use this.GetType() to determine if this Controller is already in the Dictionary
        if (!ControllerRolesDictionary.ContainsKey(this.GetType()))
        {
            // If not, use Reflection to add List of Roles to Dictionary 
            // associating with Controller
        }
    }
}

Is this doable? If so, how do I perform Reflection in the ExtController constructor to discover the [Authorize] attribute and related Roles (if any)

ALSO! Feel free to go out-of-scope on this question and suggest an alternate way of solving this "Dynamic Site.Master Menu based on Roles" problem. I'm the first to admit that this may not be the best approach.

EDIT

After much reading and experimenting, I came up with my own solution. See below for my answer. Any constructive feedback / criticism welcome!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

删除→记忆 2024-09-11 14:32:00

我更喜欢链接到菜单中的所有内容,并 创建一个 HtmlHelper 来检查链接是否存在是否可访问基于[Authorize]属性。

I prefer linking to everything in my Menus and creating a HtmlHelper which checks to see if a link is accessible or not based on the [Authorize] attributes.

久夏青 2024-09-11 14:32:00

好的,所以我决定像我最初提议的那样充实我自己的扩展控制器类。这是一个非常基本的版本。我可以看到各种让它变得更好的方法(进一步扩展,收紧代码等),但我想我会提供我的基本结果,因为我想还有很多其他人想要类似的东西,但可能不想要全部额外的。

public abstract class ExtController : Controller
{
    protected static Dictionary<string, List<string>> RolesControllerDictionary;
    protected override void OnActionExecuted(ActionExecutedContext filterContext)   
    {   
        // build list of menu items based on user's permissions, and add it to ViewData  
        IEnumerable<MenuItem> menu = BuildMenu();  
        ViewData["Menu"] = menu;
    }

    private IEnumerable<MenuItem> BuildMenu()
    {
        // Code to build a menu
        var dynamicMenu = new List<MenuItem>();
        SomeRoleProvider rp = new SomeRoleProvider();
        // ^^^^^INSERT DESIRED ROLE PROVIDER HERE^^^^^
        rp.Initialize("", new NameValueCollection());
        try
        {   // Get all roles for user from RoleProvider
            foreach (var role in rp.GetRolesForUser(HttpContext.User.Identity.Name))
            {   // Check if role is in dictionary
                if (RolesControllerDictionary.Keys.Contains(role))
                {   
                    var controllerList = RolesControllerDictionary[role];
                    foreach (var controller in controllerList)
                    {   // Add controller to menu only if it is not already added
                        if (dynamicMenu.Any(x => x.Text == controller))
                        { continue; }
                        else
                        { dynamicMenu.Add(new MenuItem(controller)); }
                    }
                }
            }
        }
        catch { }   // Most role providers can throw exceptions. Insert Log4NET or equiv here.   
        return dynamicMenu; 
    }

    public ExtController()
    {
        // Check if ControllerRolesDictionary is non-existant
        if (RolesControllerDictionary == null)
        {
            RolesControllerDictionary = new Dictionary<string, List<string>>();
            // If so, use Reflection to add List of all Roles associated with Controllers
            const bool allInherited = true;
            const string CONTROLLER = "Controller";
            var myAssembly = System.Reflection.Assembly.GetExecutingAssembly();

            // get List of all Controllers with [Authorize] attribute
            var controllerList = from type in myAssembly.GetTypes()
                                 where type.Name.Contains(CONTROLLER)
                                 where !type.IsAbstract
                                 let attribs = type.GetCustomAttributes(allInherited)
                                 where attribs.Any(x => x.GetType().Equals(typeof(AuthorizeAttribute)))
                                 select type;
            // Loop over all controllers
            foreach (var controller in controllerList)
            {   // Find first instance of [Authorize] attribute
                var attrib = controller.GetCustomAttributes(allInherited).First(x => x.GetType().Equals(typeof(AuthorizeAttribute))) as AuthorizeAttribute;
                foreach (var role in attrib.Roles.Split(',').AsEnumerable())
                {   // If there are Roles associated with [Authorize] iterate over them
                    if (!RolesControllerDictionary.ContainsKey(role))
                    { RolesControllerDictionary[role] = new List<string>(); }
                    // Add controller to List of controllers associated with role (removing "controller" from name)
                    RolesControllerDictionary[role].Add(controller.Name.Replace(CONTROLLER,""));
                }
            }
        }
    }
}

使用时,只需:

  • 将 [Authorize(Roles="SomeRole1,SomeRole2,SomeRole3,etc."] 添加到 Controller 类中
  • 将继承的“Controller”替换为“ExtController”。

例如:

[Authorize(Roles = "Biologist,Admin")]
public class BiologistController : ExtController
{
    public ActionResult Index()
    { return View(); }
}

如果不替换“Controller”使用“ExtController”,那么该控制器将不会有动态菜单(我认为这在某些情况下可能很有用......)

在我的 Site.Master 文件中,我更改了 <强>“菜单”部分看起来像这样:

<ul id="menu">              
    <li><%= Html.ActionLink("Home", "Index", "Home")%></li>
    <%  if (ViewData.Keys.Contains("Menu"))
        {
          foreach (MenuItem menu in (IEnumerable<MenuItem>)ViewData["Menu"])
          { %>
    <li><%= Html.ActionLink(menu.Text, "Index", menu.Text)%></li>           
     <%   } 
        }   
     %>       
    <li><%= Html.ActionLink("About", "About", "Home")%></li>
</ul>

就是这样:-)

Ok, so I decided to flesh out my own Extended Controller class like I originally proposed. Here is a very basic version. I can see various ways of making this better (extending further, tightening up the code, etc.) but I thought I would offer up my basic results because I imagine there are plenty of other people that want something similar, but might not want all the extras.

public abstract class ExtController : Controller
{
    protected static Dictionary<string, List<string>> RolesControllerDictionary;
    protected override void OnActionExecuted(ActionExecutedContext filterContext)   
    {   
        // build list of menu items based on user's permissions, and add it to ViewData  
        IEnumerable<MenuItem> menu = BuildMenu();  
        ViewData["Menu"] = menu;
    }

    private IEnumerable<MenuItem> BuildMenu()
    {
        // Code to build a menu
        var dynamicMenu = new List<MenuItem>();
        SomeRoleProvider rp = new SomeRoleProvider();
        // ^^^^^INSERT DESIRED ROLE PROVIDER HERE^^^^^
        rp.Initialize("", new NameValueCollection());
        try
        {   // Get all roles for user from RoleProvider
            foreach (var role in rp.GetRolesForUser(HttpContext.User.Identity.Name))
            {   // Check if role is in dictionary
                if (RolesControllerDictionary.Keys.Contains(role))
                {   
                    var controllerList = RolesControllerDictionary[role];
                    foreach (var controller in controllerList)
                    {   // Add controller to menu only if it is not already added
                        if (dynamicMenu.Any(x => x.Text == controller))
                        { continue; }
                        else
                        { dynamicMenu.Add(new MenuItem(controller)); }
                    }
                }
            }
        }
        catch { }   // Most role providers can throw exceptions. Insert Log4NET or equiv here.   
        return dynamicMenu; 
    }

    public ExtController()
    {
        // Check if ControllerRolesDictionary is non-existant
        if (RolesControllerDictionary == null)
        {
            RolesControllerDictionary = new Dictionary<string, List<string>>();
            // If so, use Reflection to add List of all Roles associated with Controllers
            const bool allInherited = true;
            const string CONTROLLER = "Controller";
            var myAssembly = System.Reflection.Assembly.GetExecutingAssembly();

            // get List of all Controllers with [Authorize] attribute
            var controllerList = from type in myAssembly.GetTypes()
                                 where type.Name.Contains(CONTROLLER)
                                 where !type.IsAbstract
                                 let attribs = type.GetCustomAttributes(allInherited)
                                 where attribs.Any(x => x.GetType().Equals(typeof(AuthorizeAttribute)))
                                 select type;
            // Loop over all controllers
            foreach (var controller in controllerList)
            {   // Find first instance of [Authorize] attribute
                var attrib = controller.GetCustomAttributes(allInherited).First(x => x.GetType().Equals(typeof(AuthorizeAttribute))) as AuthorizeAttribute;
                foreach (var role in attrib.Roles.Split(',').AsEnumerable())
                {   // If there are Roles associated with [Authorize] iterate over them
                    if (!RolesControllerDictionary.ContainsKey(role))
                    { RolesControllerDictionary[role] = new List<string>(); }
                    // Add controller to List of controllers associated with role (removing "controller" from name)
                    RolesControllerDictionary[role].Add(controller.Name.Replace(CONTROLLER,""));
                }
            }
        }
    }
}

To use, just:

  • Add the [Authorize(Roles="SomeRole1,SomeRole2,SomeRole3,etc."] to the Controller Class
  • Replace the inherited "Controller" with "ExtController".

For example:

[Authorize(Roles = "Biologist,Admin")]
public class BiologistController : ExtController
{
    public ActionResult Index()
    { return View(); }
}

If you don't replace "Controller" with "ExtController", then that Controller won't have a dynamic menu. (This could be useful, in some scenarios, I think...)

In my Site.Master file, I changed the "menu" section to look like this:

<ul id="menu">              
    <li><%= Html.ActionLink("Home", "Index", "Home")%></li>
    <%  if (ViewData.Keys.Contains("Menu"))
        {
          foreach (MenuItem menu in (IEnumerable<MenuItem>)ViewData["Menu"])
          { %>
    <li><%= Html.ActionLink(menu.Text, "Index", menu.Text)%></li>           
     <%   } 
        }   
     %>       
    <li><%= Html.ActionLink("About", "About", "Home")%></li>
</ul>

And that's it! :-)

给妤﹃绝世温柔 2024-09-11 14:32:00

我遇到了同样的问题,需要逻辑留在控制器端。但我确实喜欢约翰的方法,因为它使用系统过滤器来决定某个操作是否被授权。如果它对任何人有帮助,以下代码从 John 的方法中删除了 HtmlHelper

    protected bool HasActionPermission(string actionName, string controllerName)
    {
        if (string.IsNullOrWhiteSpace(controllerName))
            return false;

        var controller = GetControllerByName(ControllerContext.RequestContext, controllerName);
        var controllerDescriptor = new ReflectedControllerDescriptor(controller.GetType());
        var actionDescriptor = controllerDescriptor.FindAction(ControllerContext, actionName);
        return ActionIsAuthorized(ControllerContext, actionDescriptor);
    }

    private static bool ActionIsAuthorized(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
    {
        if (actionDescriptor == null)
            return false; // action does not exist so say yes - should we authorise this?!

        AuthorizationContext authContext = new AuthorizationContext(controllerContext, actionDescriptor);

        // run each auth filter until on fails
        // performance could be improved by some caching
        foreach (var filter in FilterProviders.Providers.GetFilters(controllerContext, actionDescriptor))
        {
            var authFilter = filter.Instance as IAuthorizationFilter;

            if (authFilter == null)
                continue;

            authFilter.OnAuthorization(authContext);

            if (authContext.Result != null)
                return false;
        }

        return true;
    }

    private static ControllerBase GetControllerByName(RequestContext context, string controllerName)
    {
        IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();

        IController controller = factory.CreateController(context, controllerName);

        if (controller == null)
        {
            throw new InvalidOperationException(

                String.Format(
                    CultureInfo.CurrentUICulture,
                    "Controller factory {0} controller {1} returned null",
                    factory.GetType(),
                    controllerName));
        }
        return (ControllerBase)controller;
    }

I met the same problem that requires the logic to stay in controller side. But I do like John's approach as it uses the system filter to decide if an action is authorized. In case it helps anyone, the following code removed the HtmlHelper from John's approach:

    protected bool HasActionPermission(string actionName, string controllerName)
    {
        if (string.IsNullOrWhiteSpace(controllerName))
            return false;

        var controller = GetControllerByName(ControllerContext.RequestContext, controllerName);
        var controllerDescriptor = new ReflectedControllerDescriptor(controller.GetType());
        var actionDescriptor = controllerDescriptor.FindAction(ControllerContext, actionName);
        return ActionIsAuthorized(ControllerContext, actionDescriptor);
    }

    private static bool ActionIsAuthorized(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
    {
        if (actionDescriptor == null)
            return false; // action does not exist so say yes - should we authorise this?!

        AuthorizationContext authContext = new AuthorizationContext(controllerContext, actionDescriptor);

        // run each auth filter until on fails
        // performance could be improved by some caching
        foreach (var filter in FilterProviders.Providers.GetFilters(controllerContext, actionDescriptor))
        {
            var authFilter = filter.Instance as IAuthorizationFilter;

            if (authFilter == null)
                continue;

            authFilter.OnAuthorization(authContext);

            if (authContext.Result != null)
                return false;
        }

        return true;
    }

    private static ControllerBase GetControllerByName(RequestContext context, string controllerName)
    {
        IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();

        IController controller = factory.CreateController(context, controllerName);

        if (controller == null)
        {
            throw new InvalidOperationException(

                String.Format(
                    CultureInfo.CurrentUICulture,
                    "Controller factory {0} controller {1} returned null",
                    factory.GetType(),
                    controllerName));
        }
        return (ControllerBase)controller;
    }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文