ASP.NET MVC: как найти контроллеры с атрибутами [Authorize] с помощью Reflection в C #? (или как создать динамическое меню Site.Master?)

Может быть, мне стоит сделать резервную копию и расширить рамки, прежде чем углубляться в вопрос, связанный с заголовком ...

В настоящее время я пишу веб-приложение на ASP.NET MVC 1.0 (хотя на моем компьютере установлен MVC 2.0, поэтому я не ограничен только 1.0). Я начал со стандартного проекта MVC, в котором есть ваш basic Добро пожаловать в ASP.NET MVC, и в правом верхнем углу отображаются вкладки [Главная] и [О программе]. Довольно стандартно, правда?

Я добавил 4 новых класса Контроллеров, назовем их Астроном, Биолог, Химик и Физик. К каждому новому классу контроллера прикреплен атрибут [Authorize].

Например, для файла BiologistController.cs

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

Эти теги [Authorize], естественно, ограничивают, какой пользователь может получить доступ к различным контроллерам в зависимости от ролей, но я хочу динамически создавать меню в верхней части моего веб-сайта на странице Site.Master на основе ролей, частью которых является пользователь. Так, например, если JoeUser был членом группы Roles Astronomer and Physicist, в меню навигации было бы написано:

[На главную] [Астроном] [Физик] [О себе]

И, естественно, он не перечисляет ссылки на страницу указателя контроллера биолога или химика.

Или, если бы JohnAdmin был членом Role Admin, ссылки на все 4 контроллера отобразились бы на панели навигации.

Хорошо, вы, наверное, поняли ... А теперь настоящий вопрос ...


Начиная с ответа из этой темы StackOverflow о создании динамического меню в ASP.NET, я пытаюсь понять, как я смогу полностью реализовать это. (Я новичок и нуждаюсь в дополнительных советах, так что, пожалуйста, обнажитесь со мной.)

В ответе предлагается расширить класс контроллера (назовите его ExtController), а затем унаследовать каждый новый контроллер WhateverController от ExtController.

Я пришел к выводу, что мне нужно использовать Reflection в этом конструкторе 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
        }
    }
}

Это выполнимо? Если да, то как мне выполнить Reflection в конструкторе ExtController, чтобы обнаружить атрибут [Authorize] и связанные роли (если есть)

ТАКЖЕ! Не стесняйтесь выходить за рамки этого вопроса и предлагать альтернативный способ решения этого динамического меню Site.Master на основе проблемы ролей. Я первый, кто признает, что это, возможно, не лучший подход.

ИЗМЕНИТЬ

После долгого чтения и экспериментов я пришел к собственному решению. Мой ответ смотрите ниже. Любые конструктивные отзывы / критика приветствуются!


person Pretzel    schedule 08.06.2010    source источник


Ответы (3)


Я предпочитаю делать ссылки на все в своих меню и создавать HtmlHelper, который проверяет, является ли ссылка доступны или нет в зависимости от атрибутов [Authorize].

person John Farrell    schedule 08.06.2010
comment
Я просмотрел ваш ответ и, пожалуйста, простите мое незнание, я все еще изучаю ASP.NET MVC, но в какую папку и в какой файл вы обычно вставляете эти 2 класса? Точно так же как класс SecurityTrimmedLink используется в файле Site.Master? - person Pretzel; 08.06.2010

Хорошо, поэтому я решил конкретизировать свой собственный класс расширенного контроллера, как я изначально предлагал. Вот очень простая версия. Я вижу различные способы сделать это лучше (дальнейшее расширение, ужесточение кода и т. Д.), Но я подумал, что предложу свои основные результаты, потому что я полагаю, что есть много других людей, которые хотят чего-то похожего, но могут не захотеть всего дополнительные услуги.

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 и т. Д."] В класс контроллера
  • Замените унаследованный «Контроллер» на «ExtController».

Например:

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

Если вы не замените «Контроллер» на «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>

И это все! :-)

person Pretzel    schedule 11.06.2010
comment
В качестве примечания, вероятно, было бы лучше создать Dictionary ‹Role, ControllerActionObject› на ApplicationStart, потому что классы не изменятся, пока вы не развернете заново. Тогда создание меню не будет динамическим каждый раз, а будет быстрый поиск в словаре. - person Erik Philips; 13.03.2014

Я столкнулся с той же проблемой, которая требует, чтобы логика оставалась на стороне контроллера. Но мне нравится подход Джона, поскольку он использует системный фильтр, чтобы решить, разрешено ли действие. Если это кому-то поможет, следующий код удалил 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;
    }
person Bob    schedule 29.05.2019