3 November 2012

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

Website development.NETASP
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.

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

Спасибо за внимание, надеюсь я кому-то помог.
Tags:asp.netasp.net mvcавторизацияаутентификацияweb-разработка
Hubs: Website development .NET ASP
+4
22.1k 81
Comments 18
Popular right now
Разработчик C#
from 100,000 to 160,000 ₽ТакскомМосква
С#/.NET Developer
to 100,000 ₽Teleperformance Russia GroupВолгоград
Разработчик С# ASP.NET
from 140,000 to 170,000 ₽МТСМосква
.Net Developer
to 250,000 ₽e-POSМоскваRemote job
Backend-разработчик ASP.NET Core (C#, удаленка)
from 120,000 to 150,000 ₽Digital SpartaRemote job