Пользователь
0,1
рейтинг
3 ноября 2012 в 02:43

Разработка → Использование функционала фреймворка MVC4 для авторизации пользователей и использование ролевой модели доступа к сайту tutorial

Приветствую.
Сегодня мне бы хотелось рассказать в совсем небольшом уроке (уровень скорее для очень начинающих), как можно достаточно быстро и легко настроить аутентификацию пользователей, а так же авторизацию при их доступе к некоторому функционалу на Вашем сайте, используя штатные средства фреймворка MVC(4).

Вводная

Я сейчас пишу личный простенький сайт для учета и ведения расходов, доходов, напоминания о периодических платежах (жкх, кредиты, школа и т.п.) + аналитика (в основном диаграммы), поскольку меня и мою жену функциональность Google Docs устраивать перестала.
Соответственно, встал вопрос о том, как закрыть информацию, в данном случае финансового состояния семьи от посторонних глаз под аутентификацию а так же распределить роли доступа (авторизация) — что могут жена, ребенок, анонимные пользователи, а что может администратор глава семьи.

UPD: описал способы создания пользователей, ролей более правильным способом (не надо лезть напрямую в БД)
Код, показывающий меню, стоит перевести в более правильный вид, соответствующий идеологии MVC, поскольку текущий код далек от образцового и написан быстро, для демонстрации, я над этим работаю.


Небольшое предисловие — Причины, побудившие меня написать эту статью и немного заметок для начинающих">

Проект создавался в рамках изучения C#, MVC4 — я новичок.
Я потратил несколько вечеров на поиск, возню с пользовательскими провайдерами и их настройкой, пока не понял, что весь этот код не нужен мне на данном этапе. Следствием стало переписывание статьи по когда-то вбитому мне в голову принципу — чем меньше изменений вносится в любой объект, будь то конфигурационный файл, документ или код на текущем уровне моих знаний либо представлений, тем потом будет проще. Возможно я упустил какие-то важные нюансы (я начинающий ), поэтому буду рад как критике, так и подсказкам аудитории.

Я считаю, что пользователь уже создал хотя бы один простой сайт на данном фреймворке, например, воспользовавшись базовой инструкцией по созданию простого каталога Ваших фильмов на фреймворке MVC4 (у меня ушло около 20 минут).
Или же изучая MVC по второй, очень хорошей и более сложной инструкцией, по созданию онлайн магазина продажи музыкальных альбомов, (она касается MVC3, но, тем не менее, изучение MVC я рекомендую начинать с данной инструкции).

В процессе изучения второй инструкции я натолкнулся на проблемы, связанные с тем, что некоторые вещи в MVC4 изменились, по сравнению с MVC3, и брать устаревший код контроллера от предыдущей модели фреймворка и модели считаю плохой идеей, поэтому решил разобраться с этой задачей.

Предварительное условие:

При создании нового проекта MVC4 по умолчанию вверху, справа при открытии сайта есть небольшое меню из двух пунктов — «Регистрация» и «Выполнить вход».


Техническое задание:

«Активировать» и запустить на нашем сайте ролевую модель доступа с распределением функционала.

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

Рабочая среда:

У меня стоят привычные мне русскоязычные варианты Visual Studio 2012 Express, .Net 4.5, SQL 2012 и MVC4 (а так же TFS2012 Express. Всё это живет на Windows 2008R2), в случае установки у Вас английской локализации названия, пункты меню и другие элементы интерфейса будут называться по другому, поэтому я по максимуму абстрагировался от скриншотов экрана.

Решение задачи



Подготовка хранилища

Я предпочитаю отделять данные приложения от авторизации, поэтому создал отдельную базу данных users,

  1. В каталоге App_Data надо создать новую, пустую базу данных, назовём её «users» для идентификации её содержимого.
  2. В нашем web приложении надо описать подключение к нашей новой базе данных, для этого надо открыть файл web.config, который находится в корневом каталоге нашего приложения, найти (обычно в самом начале файла), пункт, описывающий соединения с источниками данных. (я взял конфиг из уже работающего сайта, поэтому у Вас верным и совпадающим пунктом будет только имя соединения DefaultConnection).
    <connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=aspnet-MyMoney-2132141343;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\users.mdf" providerName="System.Data.SqlClient" />
    <add name="payDBContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=aspnet-MyMoney-data;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\MyMoney.mdf" providerName="System.Data.SqlClient" />
    </connectionStrings>
    

    Немного подробнее:
    В данном списке подключений у нас есть соединение по умолчанию «DefaultConnection», у нас оно будет использоваться только для хранения пользовательской информации, поэтому мы это менять не будем, все данные приложения будут храниться в другой базе данных, payDBContext.
    В настройках DefaultConnection мы меняем:
    1. Начальный каталог «Initial Catalog=aspnet-MyMoney-2132141343», это имя базы данных в момент прикрепления БД к серверу БД, поэтому в случае работы нескольких баз данных с этим одинаковым именем, могут быть неоднозначности
    2. пункт «AttachDBFilename=|DataDirectory|\users.mdf», имя файла базы данных, где будет храниться информация о пользователях и ролях. Имя базы данных, созданной нами в каталоге App_Data, «users.mdf».

  3. Теперь можно запустить сайт, зайти и зарегистрировать, например двух пользователей Admin и User
  4. После чего в обозревателе баз данных прямо в студии открываем структуру нашей БД

    И получим нечто подобное
  5. Для добавления ролей можно в accountmodel.cs добавить такой код:
        public class UsersContext : DbContext
        {
            public UsersContext()
                : base("users")
            {
            }
    
            public DbSet<UserProfile> UserProfiles { get; set; }
            public DbSet<webpages_Roles> webpages_Roles { get; set; } // добавляем таблицу ролей
            }
    
    // описание таблицы ролей
        [Table("webpages_Roles")]
        public class webpages_Roles
        {
            [Key]
            [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
            public int RoleId { get; set; }
            [Display(Name = "Имя роли")]
            public string RoleName { get; set; }
        }
    

    Тогда мы сможем после пересборки проекта создать контроллер roles (не забудьте закрыть его для пользователей с административной ролью)


    Пример добавления пользователей лучше посмотреть в контроллере «Account», методы Register и модицифировать его для добавления пользователя в одну или несколько ролей, используя Roles.AddUserToRole(), например, закрыв для администраторской роли, либо добавить свой контроллер. который будет добавлять пользователя в роль.


У меня за работу с финансовой информацией отвечает несколько контроллеров, поэтому в описании каждого контроллера (можно закрыть или наоборот, открыть отдельные методы) надо вставить соответствующую настройку доступа:
[Authorize(Roles = "Admin")]

namespace MyMoney.Controllers
{
    [Authorize(Roles = "Admin")]
    public class catController : Controller
    {

Или же вставить такую строку для входа в методы нескольких ролей
[Authorize(Roles = "Admin, User")]

Теперь я хочу в зависимости от роли пользователя немного изменить список меню.
  1. В каталоге /Views/Shared я создаю частичное представление (оно же Partial View) с названием "_Menu"
    исходный код, кстати, кто подскажет как его лучше оптимизировать, а то утянул
    @{
        
        var menus = new[]
                    {
                       new { LinkText="На главную", ActionName="Index",ControllerName="Home",Roles="All" },
                       new { LinkText="О себе", ActionName="About",ControllerName="Home",Roles="All" },
                       new { LinkText="Контакты", ActionName="Contact",ControllerName="Home",Roles="All" },
                       new { LinkText="Финансы", ActionName="Index",ControllerName="payments",Roles="Admin,User" },
                    };
    }
    
    <ul id="menu">
    @if (HttpContext.Current.User.Identity.IsAuthenticated)
    {
        String[] roles = Roles.GetRolesForUser();
        var links = from item in menus
                    where item.Roles.Split(new String[] { "," }, StringSplitOptions.RemoveEmptyEntries)
                    .Any(x => roles.Contains(x) || x == "All")
                    select item;
        foreach (var link in links)
        {
            @: <li> @Html.ActionLink(link.LinkText, link.ActionName,link.ControllerName)</li>
        }
    }
    else{
        var links = from item in menus
                    where item.Roles.Split(new String[]{","},StringSplitOptions.RemoveEmptyEntries)
                    .Any(x=>new String[]{"All","Anonymous"}.Contains(x))
                    select item;
         foreach ( var link in links){
             @: <li> @Html.ActionLink(link.LinkText, link.ActionName, link.ControllerName)</li>
         }
    }
    </ul>
    		


  2. Теперь надо его подключить в разметку /Views/Shared/_Layout.cshtml

    Ищем в файле _Layout.cshtml этот код
       <section id="login">
                        @Html.Partial("_LoginPartial")
                    </section>
                    <nav>
            <ul id="navlist">
                <li class="first"><a href="@Url.Content("~")" id="current">Home</a></li>
                <li><a href="@Url.Content("~/Store/")">Store</a></li>
                <li>@{Html.RenderAction("CartSummary", "ShoppingCart");}</li>
                <li><a href="@Url.Content("~/StoreManager/")">Admin</a></li>
            </ul>        
                    </nav>
    


    и меняем блок
    <nav> ... </nav>
    

    на
    <nav>
                @Html.Partial("~/Views/Shared/_Menu.cshtml")
    </nav>
                

  3. У меня есть один контроллер с коротким именем, использующий одно представление как меню, вот код представления.
    Набросал по быстрому, чтобы показать работу с ролями.
    код представления
    @{
        var menus = new[]
                    {
                       new { LinkText="Home", ActionName="Index",ControllerName="Home",Roles="All"  },
                       new { LinkText="About", ActionName="About",ControllerName="Home",Roles="Anonymous"  },
                       new { LinkText="Contact", ActionName="Contact",ControllerName="Home",Roles="Anonymous"  },
                       
                       new { LinkText="Добавить платёж", ActionName="Create",ControllerName="pay",Roles="Admin,User"  },
                       new { LinkText="Просмотр платежей", ActionName="Index",ControllerName="pay",Roles="Admin,User"  },
    
                       new { LinkText="Добавить категорию", ActionName="Create",ControllerName="cat",Roles="Admin"  },
                       new { LinkText="Просмотр категорий", ActionName="Index",ControllerName="cat",Roles="Admin,User"  },
                       
                       new { LinkText="Добавить пользователя", ActionName="Create",ControllerName="user",Roles="Admin"  },
                       new { LinkText="Просмотр пользователей", ActionName="Index",ControllerName="user",Roles="Admin"  },
                       
                       new { LinkText="Добавить тип", ActionName="Create",ControllerName="type",Roles="Admin"  },
                       new { LinkText="Просмотр типов", ActionName="Index",ControllerName="type",Roles="Admin"  },
                       //new { LinkText="", ActionName="",ControllerName="",Roles="Administrator"  },
                    };
    }
    
    @{
        ViewBag.Title = "Управление финансами";
    }
    
    <h2>Управление финансами</h2>
    
    <p>Вы: @User.Identity.Name</p>
    <p>Вы входите в группы:</p>
    <table>
        <tr>
            @{ foreach (string item in Roles.GetRolesForUser())
               {
                   <li>@item</li>
               }
            }
        </tr>
    </table>
    
    
    <ul id="list">
    
        @if (HttpContext.Current.User.Identity.IsAuthenticated)
        {
            String[] roles = Roles.GetRolesForUser();
            var links = from item in menus
                        where item.Roles.Split(new String[] { "," }, StringSplitOptions.RemoveEmptyEntries)
                        .Any(x => roles.Contains(x) || x == "All")
                        select item;
            foreach (var link in links)
            {
            @: <li> @Html.ActionLink(link.LinkText, link.ActionName, link.ControllerName)</li>
        }
        }
        else
        {
            var links = from item in menus
                        where item.Roles.Split(new String[] { "," }, StringSplitOptions.RemoveEmptyEntries)
                        .Any(x => new String[] { "All", "Anonymous" }.Contains(x))
                        select item;
            foreach (var link in links)
            {
            @: <li> @Html.ActionLink(link.LinkText, link.ActionName, link.ControllerName)</li>
         }
        }
    </ul>
    




Теперь надо очистить, пересобрать приложение и можно запускать. Если зная ссылку на один из контроллеров, закрытых для анонимного входа, попробовать зайти — получите форму авторизации с последующим обратным переходом на страничку, которая являлась инициатором авторизации.
Выглядит это так:

Анонимный вход



Вход с административной ролью



Вход с ролью пользователя


Замечания напоследок
О чем я сейчас думаю — надо в свою БД финансов привязать получение данных текущего авторизованного пользователя, т.к. сейчас в гугль докс приходится руками выбирать, кто был инициатором расхода/дохода — а вводить два раза информацию глупо. А в идеале — получать авторизацию и из Windows сессии, если броузер IE.

Да, за некоторые фрагменты кода меня, как начинающего надо больно бить по рукам, поэтому я с удовольствием приму поправки и улучшения данного кода.

Спасибо за внимание, надеюсь я кому-то помог.
Nikolay Turnaviotov @foxmuldercp
карма
9,0
рейтинг 0,1
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (18)

  • +1
    Если раскрыть все спойлеры кроме тех, которые с кодом меню, которого там не должно было быть, то статья будет не такой и большой, зато не нужно лишний раз кликать, чтобы увидеть маленькую картинку., Представление из примерно 10-15 строк html превратилось в 100, что ломает сам паттерн MVC. Если хочется редактировать на лету, то можно воспользоваться тем же xml, базой данных, но не хардкодить анонимные объекты и потом ещё с помощью linq что-то выбирать, представление должно быть простым без подобной логики. По поводу двух баз данных я не понял задумку, чего уж скрывать я не очень и понимаю как будет организована вся работа с ней, в одной пользователи, в другой деньги, как целостность будет организована? связи? каскадное удаление?
    • 0
      Одна БД используется только для авторизации, во второй хранятся все данные — я сторонник отделять мух от мяса.
      Но пока я действительно не думал, как мне в список расходов записывать логин того, кто этот расход делал.
      • 0
        С другой стороны, логин авторизованного пользователя у нас есть и без БД.
      • 0
        Целостность нарушается. Что если есть документы, которые он создавал (и в которых он отмечен как автор) остались и пользователя удаляют?
        • 0
          Логичный вопрос — но учитывая специфику данного проекта — пользователь врядли будет удаляться.
  • –1
    Заведи модельку, которая из куков либо из того же HttpContext.Current.User будет брать роль пользователя и отдавать во вьюшку список доступных пунктов меню.
    • 0
      Спасибо за идею.
  • +1
    WTF? Я прямо даже не знаю, что тут хуже.

    Прямое обращение к данным из представления? Логика в представлении? Прямое использование HttpContext.Current и Roles? Подключение к БД под windows-эккаунтом?
    • 0
      1,2,3 — Код был написан по быстрому и сейчас я думаю, как лучше этот код переписать.
      4 А в чем, собственно, проблема?
      • +1
        Код был написан по быстрому и сейчас я думаю, как лучше этот код переписать.

        А зачем показывать плохой быстро написанный код?

        А в чем, собственно, проблема?

        А под каким эккаунтом крутится сайт на IIS?
  • +1
    А у вас родственники в Индии есть?
  • –1
    Я как раз взялся за разработку простенького сервиса, требующего регистрации и был крайне удивлен невозможностью кастомизации стандартного механизма аутентификации/авторизации в ASP.NET MVC.
    В частности нужна поддержка пользователей через мою собственную таблицу без ролей. Также весь функционал поддержки пользователей должен работать через аякс (т.е. страница у сервиса будет всего одна).
    Так вот — эта задача для меня оказалась сложнее задачи реализации основного функционала.
    Думаю это может стать препятствием на пути миграции существующих проектов на ASP.NET MVC.
    • 0
      Вы очень плохо смотрели.
      Штатный, описанный мной функционал делается за совсем смешное время, чуть больше, если писать самостоятельно.
      А есть такая штука как «custom membershipprovider», например, который дает авторизацию, например по active directory, по ней куча статей
      • 0
        Да нормально вроде я смотрел. Единственное что нашел, это готовый каркас проекта на github (хотя он под asp.net mvc 3, но думаю подойдет).
        Здесь например советуют использовать SqlMembershipProvider, что не очень то, что хотелось-бы.
    • 0
      был крайне удивлен невозможностью кастомизации стандартного механизма аутентификации/авторизации в ASP.NET MVC

      Простите, вы какой «стандартный механизм» имеете в виду?

      А то у asp.net mvc собственного механизма аутентификации нет вообще, они используют механизмы от asp.net; а те, в свою очередь, подстраиваются очень легко — можно взять Forms-аутентификацию, где вообще вся логика отдается вам, можно реализовать свой собственный membership provider, можно подключить Claims-based-аутентификацию, а в той уже либо реализовать свой собственный провайдер аутентификации или сделать свой STS. Короче, вариантов настройки дофига.

      Что же касается собственного механизма asp.net mvc, то он есть только для авторизации, и он тоже настраивается гибче некуда — и локальные фильтры, задаваемые атрибутами, и глобальные фильтры, задаваемые фабрикой, и специальные методы в контроллере… тоже куча возможностей.

      Думаю это может стать препятствием на пути миграции существующих проектов на ASP.NET MVC.

      Миграции откуда? С WebForms? Мигрирует один-в-один, все механизмы просто сохраняюстя. Или откуда?
      • 0
        Вот этот.
        К сожалению нету возможности изучать вопрос досконально. Есть идея и есть желание побыстрее ее реализовать.
        Еще раз что не совсем тривиально (для меня во всяком случае) — реализовать кастомный (или модифицировать то, что предлагает визард) механизм аутентификации, который бы использовал одну кастомною таблицу.
        • 0
          Вот этот.

          Если прочитать внимательно, то сразу становится видно, что он не от .mvc, а от webmatrix.

          Еще раз что не совсем тривиально (для меня во всяком случае) — реализовать кастомный [...] механизм аутентификации, который бы использовал одну кастомною таблицу.

          А в чем проблема? Берете Membership Provider, все ненужные методы затыкаете эксепшенами, в оставшихся, кажется, двух пишете нужный код доступа к таблице. Все. Работы часа на два, наверное. Ссылка выше.
          • 0
            Ok. Спасибо за ссылку.

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