如何使用C#中的Reflection使用[Authorize]属性查找控制器? (或如何构建动态Site.Master菜单?)

时间:2022-02-27 19:05:52

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?

我目前正在ASP.NET MVC 1.0中编写一个Web应用程序(虽然我的PC上安装了MVC 2.0,所以我并不完全限于1.0) - 我已经开始使用标准的MVC项目了基本的“欢迎使用ASP.NET MVC”,并在右上角显示[Home]选项卡和[About]选项卡。很标准,对吗?

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.

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

For example, for the BiologistController.cs

例如,对于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:

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

[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.

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

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

好吧,你们大家都有了想法...现在回答真正的问题......


Starting with the answer from this * topic about Dynamic Menu building in ASP.NET, I'm trying to understand how I would fully implement this.

从*主题关于ASP.NET中动态菜单构建的答案开始,我试图理解如何完全实现它。

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

答案建议扩展Controller类(称之为“ExtController”),然后让每个新的WhateverController继承自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.

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

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)

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

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.

也!在这个问题上随意超出范围并建议另一种方法来解决这个“基于角色的动态Site.Master菜单”问题。我是第一个承认这可能不是最佳方法的人。

EDIT

编辑

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

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

2 个解决方案

#1


3  

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.

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

#2


3  

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
  • 将[Authorize(Roles =“SomeRole1,SomeRole2,SomeRole3等”)添加到Controller类
  • Replace the inherited "Controller" with "ExtController".
  • 用“ExtController”替换继承的“Controller”。

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...)

如果不将“Controller”替换为“ExtController”,则该Controller将没有动态菜单。 (在某些情况下,这可能很有用,我认为...)

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

在我的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>

And that's it! :-)

就是这样! :-)

#1


3  

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.

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

#2


3  

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
  • 将[Authorize(Roles =“SomeRole1,SomeRole2,SomeRole3等”)添加到Controller类
  • Replace the inherited "Controller" with "ExtController".
  • 用“ExtController”替换继承的“Controller”。

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...)

如果不将“Controller”替换为“ExtController”,则该Controller将没有动态菜单。 (在某些情况下,这可能很有用,我认为...)

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

在我的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>

And that's it! :-)

就是这样! :-)