Авторизация на основе ролей (C#)

Скотт Митчелл

Примечание

С момента написания этой статьи поставщики членства ASP.NET были заменены ASP.NET Identity. Мы настоятельно рекомендуем обновить приложения для использования платформы ASP.NET Identity , а не поставщиков членства, которые были представлены на момент написания этой статьи. ASP.NET Identity имеет ряд преимуществ по сравнению с системой членства ASP.NET, в том числе :

  • более высокая производительность;
  • Улучшенная расширяемость и тестируемость
  • Поддержка OAuth, OpenID Connect и двухфакторной проверки подлинности
  • Поддержка удостоверений на основе утверждений
  • Улучшенное взаимодействие с ASP.Net Core

Скачать код или скачать PDF-файл

Это руководство начинается с того, как платформа ролей связывает роли пользователя с контекстом безопасности. Затем в нем рассматривается применение правил авторизации URL-адресов на основе ролей. После этого мы рассмотрим использование декларативных и программных средств для изменения отображаемых данных и функциональных возможностей, предлагаемых ASP.NET страницей.

Введение

В руководстве по авторизации на основе пользователей мы узнали, как использовать авторизацию по URL-адресу, чтобы указать, какие пользователи могут посещать определенный набор страниц. С помощью немного разметки в Web.configмы можем указать ASP.NET разрешить только прошедшим проверку подлинности пользователям посещать страницу. Или можно диктовать, что разрешены только пользователи Тито и Боб, или указать, что разрешены все пользователи, прошедшие проверку подлинности, за исключением Сэма.

Помимо авторизации по URL-адресу, мы также рассмотрели декларативные и программные методы управления отображаемыми данными и функциями, предлагаемыми страницей на основе посещения пользователем. В частности, мы создали страницу со списком содержимого текущего каталога. Любой пользователь может посетить эту страницу, но только пользователи, прошедшие проверку подлинности, могут просматривать содержимое файлов, и только Tito может удалить файлы.

Применение правил авторизации для каждого пользователя может превратиться в кошмар бухгалтерского учета. Более поддерживаемым подходом является использование авторизации на основе ролей. Хорошей новостью является то, что имеющиеся в нашем распоряжении средства для применения правил авторизации одинаково хорошо работают с ролями, как и для учетных записей пользователей. Правила авторизации URL-адресов могут указывать роли вместо пользователей. Элемент управления LoginView, который отображает различные выходные данные для прошедших проверку подлинности и анонимных пользователей, можно настроить для отображения различного содержимого в зависимости от ролей пользователя, выполнившего вход. API ролей включает методы для определения ролей вошедшего пользователя.

Это руководство начинается с того, как платформа ролей связывает роли пользователя с контекстом безопасности. Затем в нем рассматривается применение правил авторизации URL-адресов на основе ролей. После этого мы рассмотрим использование декларативных и программных средств для изменения отображаемых данных и функциональных возможностей, предлагаемых ASP.NET страницей. Приступим к работе!

Общие сведения о связи ролей с контекстом безопасности пользователя

Всякий раз, когда запрос входит в конвейер ASP.NET, он связывается с контекстом безопасности, который включает сведения, идентифицирующие запрашивающего. При использовании проверки подлинности с помощью форм в качестве маркера удостоверения используется билет проверки подлинности. Как мы говорили в учебнике Обзор проверки подлинности на основе форм , FormsAuthenticationModule объект отвечает за определение удостоверения запрашивающей стороны, что и во время AuthenticateRequest события.

При обнаружении допустимого билета проверки подлинности с истекшим сроком FormsAuthenticationModule действия он декодируется для определения личности запрашивающей стороны. Он создает новый GenericPrincipal объект и назначает его объекту HttpContext.User . Цель субъекта, например GenericPrincipal, — определить имя пользователя, прошедшего проверку подлинности, и определить, к каким ролям он принадлежит. Эта цель очевидна тем фактом, что все основные объекты имеют Identity свойство и IsInRole(roleName) метод . Однако FormsAuthenticationModuleобъект не заинтересован в записи сведений о роли, а в создаваемом GenericPrincipal им объекте не указываются роли.

Если платформа ролей включена, RoleManagerModule http-модуль выполняет шаги после FormsAuthenticationModule и определяет роли пользователя, прошедшего проверку подлинности, во время PostAuthenticateRequest события, которое срабатывает после AuthenticateRequest события. Если запрос получен от пользователя, прошедшего проверку подлинности RoleManagerModule , объект перезаписывает GenericPrincipal объект, созданный FormsAuthenticationModule объектом , и заменяет его RolePrincipal объектом . Класс RolePrincipal использует API ролей, чтобы определить, к каким ролям принадлежит пользователь.

На рисунке 1 показан рабочий процесс конвейера ASP.NET при использовании проверки подлинности на основе форм и платформы ролей. Сначала FormsAuthenticationModule выполняется, идентифицирует пользователя с помощью запроса проверки подлинности и создает новый GenericPrincipal объект. Далее шаги RoleManagerModule в и перезаписывают GenericPrincipal объект RolePrincipal объектом .

Если анонимный пользователь посещает сайт, ни FormsAuthenticationModuleRoleManagerModule объект субъекта не создает.

События конвейера ASP.NET для прошедшего проверку подлинности пользователя при использовании проверки подлинности на основе форм и платформы ролей

Рис. 1. События конвейера ASP.NET для пользователя, прошедшего проверку подлинности на основе форм, и платформа ролей (щелкните для просмотра полноразмерного изображения)

Метод RolePrincipal объекта IsInRole(roleName) вызывается Roles.GetRolesForUser для получения ролей для пользователя, чтобы определить, является ли пользователь членом roleName. При использовании SqlRoleProviderэто приводит к запросу к базе данных хранилища ролей. При использовании правил RolePrincipalIsInRole авторизации URL-адресов на основе ролей метод будет вызываться при каждом запросе к странице, защищенной правилами авторизации URL-адресов на основе ролей. Вместо того, чтобы искать сведения о ролях в базе данных при каждом запросе, платформа ролей включает возможность кэширования ролей пользователя в файле cookie.

Если платформа ролей настроена для кэширования ролей пользователя в файле cookie, RoleManagerModule создается файл cookie во время события конвейера EndRequestASP.NET. Этот файл cookie используется в последующих запросах в PostAuthenticateRequest, то есть при RolePrincipal создании объекта . Если файл cookie действителен и не истек, данные в файле cookie анализируются и используются для заполнения ролей пользователя, тем самым избавляя RolePrincipal от необходимости выполнять вызов Roles к классу для определения ролей пользователя. На рисунке 2 показан этот рабочий процесс.

Сведения о роли пользователя могут храниться в файле cookie для повышения производительности

Рис. 2. Сведения о роли пользователя могут храниться в файле cookie для повышения производительности (щелкните для просмотра полноразмерного изображения)

По умолчанию механизм файлов cookie кэша роли отключен. Его можно включить с помощью разметки <roleManager> конфигурации в Web.config. Мы обсуждали использование <roleManager> элемента для указания поставщиков ролей в учебнике По созданию ролей и управлению ими, поэтому этот элемент уже должен быть в файле приложенияWeb.config. Параметры файла cookie кэша роли указываются как атрибуты <roleManager> элемента и суммируются в таблице 1.

Примечание

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

Свойство Описание
cacheRolesInCookie Логическое значение, указывающее, используется ли кэширование файлов cookie. По умолчанию — false.
cookieName Имя файла cookie кэша роли. Значение по умолчанию — . ASPXROLES".
cookiePath Путь к файлу cookie имени ролей. Атрибут path позволяет разработчику ограничить область файла cookie определенной иерархией каталогов. Значение по умолчанию — "/", которое сообщает браузеру о том, что нужно отправить файл cookie билета проверки подлинности в любой запрос, сделанный к домену.
cookieProtection Указывает, какие методы используются для защиты файла cookie кэша роли. Допустимые значения: All (по умолчанию); Encryption; None; и Validation.
cookieRequireSSL Логическое значение, указывающее, требуется ли SSL-подключение для передачи файла cookie проверки подлинности. Значение по умолчанию — false.
cookieSlidingExpiration Логическое значение, указывающее, сбрасывается ли время ожидания файла cookie каждый раз, когда пользователь посещает сайт в течение одного сеанса. Значение по умолчанию — false. Это значение уместно только в том случае, если createPersistentCookie задано значение true.
cookieTimeout Указывает время (в минутах), по истечении которого истекает срок действия файла cookie билета проверки подлинности. Значение по умолчанию — 30. Это значение уместно только в том случае, если createPersistentCookie задано значение true.
createPersistentCookie Логическое значение, указывающее, является ли файл cookie кэша роли файлом cookie сеанса или постоянным файлом cookie. Если false (по умолчанию), используется файл cookie сеанса, который удаляется при закрытии браузера. Если trueзадано значение , используется постоянный файл cookie, срок действия которого истекает в минутах cookieTimeout после создания или после предыдущего посещения в зависимости от значения cookieSlidingExpiration.
domain Указывает значение домена файла cookie. Значением по умолчанию является пустая строка, которая заставляет браузер использовать домен, из которого он был выдан (например , www.yourdomain.com). В этом случае файл cookie не будет отправляться при выполнении запросов к поддоменам, таким как admin.yourdomain.com. Если вы хотите, чтобы файл cookie передавался всем поддоменам, необходимо настроить domain атрибут , установив для него значение "yourdomain.com".
maxCachedResults Указывает максимальное количество имен ролей, кэшированных в файле cookie. Значение по умолчанию — 25. не RoleManagerModule создает файл cookie для пользователей, принадлежащих не только maxCachedResults ролям. Следовательно, RolePrincipal метод объекта IsInRole будет использовать Roles класс для определения ролей пользователя. Причина maxCachedResults заключается в том, что многие агенты пользователей не разрешают файлы cookie размером более 4096 байт. Таким образом, это ограничение предназначено для снижения вероятности превышения этого ограничения размера. Если у вас очень длинные имена ролей, может потребоваться указать меньшее maxCachedResults значение; наоборот, если у вас очень короткие имена ролей, вы, вероятно, можете увеличить это значение.

Таблица 1. Параметры конфигурации файлов cookie кэша ролей

Давайте настроим приложение для использования файлов cookie кэша непостояных ролей. Для этого обновите <roleManager> элемент в , Web.config чтобы включить следующие атрибуты, связанные с файлами cookie:

<roleManager enabled="true"    
          defaultProvider="SecurityTutorialsSqlRoleProvider"    
          cacheRolesInCookie="true"    
          createPersistentCookie="false"    
          cookieProtection="All">    

     <providers>    
     ...    
     </providers>    
</roleManager>

Элемент обновлен <roleManager> путем добавления трех атрибутов: cacheRolesInCookie, createPersistentCookieи cookieProtection. Если задать значение cacheRolesInCookietrue, RoleManagerModule теперь будет автоматически кэшировать роли пользователя в файле cookie вместо того, чтобы искать сведения о роли пользователя по каждому запросу. Я явно задал атрибутам createPersistentCookiefalse и cookieProtection значение и Allсоответственно. Технически мне не нужно было указывать значения для этих атрибутов, так как я просто назначил им значения по умолчанию, но я поместил их здесь, чтобы четко понять, что я не использую постоянные файлы cookie и что файл cookie зашифрован и проверен.

Вот и все! В дальнейшем платформа ролей будет кэшировать роли пользователей в файлах cookie. Если браузер пользователя не поддерживает файлы cookie или если файлы cookie удаляются или теряются, это не имеет большого значения — RolePrincipal объект будет просто использовать Roles класс в случае, если файл cookie (или недопустимый или просроченный) недоступен.

Примечание

Группа Microsoft Patterns & Practices не рекомендует использовать файлы cookie кэша постоянных ролей. Так как владение файлом cookie кэша ролей достаточно для подтверждения членства в роли, если злоумышленник может каким-то образом получить доступ к файлу cookie действительного пользователя, он может олицетворять этого пользователя. Вероятность этого увеличивается, если файл cookie сохраняется в браузере пользователя. Дополнительные сведения об этой рекомендации по безопасности, а также о других проблемах безопасности см. в разделе Список вопросов безопасности для ASP.NET 2.0.

Шаг 1. Определение Role-Based правил авторизации URL-адресов

Как описано в руководстве по авторизации на основе пользователя , авторизация ПО URL-адреса позволяет ограничить доступ к набору страниц на основе пользователя или роли. Правила авторизации URL-адресов изложены в Web.config статье Использование <authorization> элемента с дочерними <allow> элементами и <deny> . В дополнение к правилам авторизации, связанным с пользователем, описанным в предыдущих руководствах, каждый <allow> дочерний элемент и <deny> могут также включать:

  • Определенная роль
  • Список ролей с разделителями-запятыми

Например, правила авторизации URL-адресов предоставляют доступ пользователям с ролями "Администраторы" и "Супервизоры", но запрещают доступ всем остальным:

<authorization>
     <allow roles="Administrators, Supervisors" />
     <deny users="*" />
</authorization>

Элемент <allow> в приведенной выше разметке указывает, что роли Администраторы и Супервизоры разрешены; <deny> элемент указывает, что все пользователи запрещены.

Давайте настроим приложение таким образом, чтобы ManageRoles.aspxстраницы , UsersAndRoles.aspxи CreateUserWizardWithRoles.aspx были доступны только тем пользователям с ролью "Администраторы", а RoleBasedAuthorization.aspx страница оставалась доступной для всех посетителей.

Для этого сначала добавьте Web.config файл в папку Roles .

Добавление файла Web.config в каталог ролей

Рис. 3. Добавление Web.config файла в Roles каталог (щелкните, чтобы просмотреть полноразмерное изображение)

Затем добавьте следующую разметку конфигурации в Web.config:

<?xml version="1.0"?>    

<configuration>    
     <system.web>    
          <authorization>    
               <allow roles="Administrators" />    
               <deny users="*"/>    
          </authorization>    

     </system.web>

     <!-- Allow all users to visit RoleBasedAuthorization.aspx -->    
     <location path="RoleBasedAuthorization.aspx">    
          <system.web>    
               <authorization>    
                    <allow users="*" />    

               </authorization>    
          </system.web>    
     </location>    
</configuration>

Элемент <authorization> в <system.web> разделе указывает, что доступ к ASP.NET ресурсам в каталоге могут получить только пользователи с ролью Roles "Администраторы". Элемент <location> определяет альтернативный набор правил авторизации URL-адресов для RoleBasedAuthorization.aspx страницы, позволяя всем пользователям посещать страницу.

После сохранения изменений Web.configв войдите в систему от имени пользователя, не являющегося администратором, и попытайтесь посетить одну из защищенных страниц. будет UrlAuthorizationModule обнаруживать, что у вас нет разрешения на посещение запрошенного ресурса; следовательно, FormsAuthenticationModule будет перенаправлять вас на страницу входа. Страница входа перенаправит вас на страницу UnauthorizedAccess.aspx (см. рис. 4). Последнее перенаправление со страницы входа на UnauthorizedAccess.aspx происходит из-за кода, который мы добавили на страницу входа в шаге 2 руководства по авторизации на основе пользователя. В частности, страница входа автоматически перенаправляет любого пользователя, прошедшего проверку подлинности, на страницу UnauthorizedAccess.aspx , если строка запроса содержит ReturnUrl параметр, так как этот параметр указывает, что пользователь прибыл на страницу входа после попытки просмотреть страницу, на которую он не был авторизован.

Только пользователи с ролью

Рис. 4. Только пользователи с ролью "Администраторы" могут просматривать защищенные страницы (щелкните для просмотра полноразмерного изображения)

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

Тито может посетить страницу UsersAndRoles.aspx, так как он имеет роль

Рис. 5. Тито может посетить страницу UsersAndRoles.aspx , так как он находится в роли администраторов (щелкните для просмотра полноразмерного изображения)

Примечание

При указании правил авторизации URL-адресов для ролей или пользователей важно помнить, что правила анализируются по одному сверху вниз. Как только совпадение найдено, пользователю предоставляется или запрещается доступ в зависимости от того, было ли найдено совпадение в элементе <allow> или <deny> . Если совпадение не найдено, пользователю предоставляется доступ. Следовательно, если вы хотите ограничить доступ одной или несколькими учетными записями пользователей <deny> , необходимо использовать элемент в качестве последнего элемента в конфигурации авторизации URL-адреса. Если правила авторизации URL-адресов не включают<deny>элемент, всем пользователям будет предоставлен доступ. Более подробное обсуждение того, как анализируются правила авторизации URL-адресов, см. в разделе "Как UrlAuthorizationModule использовать правила авторизации для предоставления или запрета доступа" руководства по авторизации на основе пользователя.

Шаг 2. Ограничение функциональности на основе ролей вошедшего в систему пользователя

Авторизация ПО URL-адреса позволяет легко указать грубые правила авторизации, которые указывают, какие удостоверения разрешены и какие из них запрещено просматривать определенную страницу (или все страницы в папке и ее вложенных папках). Однако в некоторых случаях может потребоваться разрешить всем пользователям посещать страницу, но ограничить функциональность страницы в зависимости от ролей посещающих пользователей. Это может повлечь за собой отображение или скрытие данных на основе роли пользователя или предоставление дополнительных функций пользователям, принадлежащим к определенной роли.

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

Давайте создадим страницу со списком всех учетных записей пользователей в системе в GridView. GridView будет включать имя пользователя, адрес электронной почты, дату последнего входа и комментарии о пользователе. Помимо отображения сведений о каждом пользователе, GridView будет включать возможности редактирования и удаления. Изначально мы создадим эту страницу с функцией редактирования и удаления, доступной всем пользователям. В разделах "Использование элемента управления LoginView" и "Программное ограничение функциональности" мы посмотрим, как включить или отключить эти функции в зависимости от роли посещающего пользователя.

Примечание

На странице ASP.NET, который мы собираемся создать, для отображения учетных записей пользователей используется элемент управления GridView. Так как эта серия руководств посвящена проверке подлинности форм, авторизации, учетным записям пользователей и ролям, я не хочу тратить слишком много времени на обсуждение внутренней работы элемента управления GridView. Хотя в этом руководстве содержатся пошаговые инструкции по настройке этой страницы, в нем не рассматривается, почему были сделаны определенные варианты и какое влияние определенные свойства оказывают на отображаемые выходные данные. Для тщательного изучения элемента управления GridView проверка мою серию руководств По работе с данными в ASP.NET 2.0.

Начните с открытия страницы RoleBasedAuthorization.aspx в папке Roles . Перетащите элемент GridView со страницы на Designer и задайте для нее ID значение UserGrid. Через некоторое время мы напишем код, который вызывает Membership.GetAllUsers метод и привязывает результирующий MembershipUserCollection объект к GridView. содержит MembershipUserCollection объект для каждой учетной MembershipUser записи пользователя в системе; MembershipUser объекты имеют такие свойства, как UserName, Email, LastLoginDateи т. д.

Перед написанием кода, который привязывает учетные записи пользователей к сетке, давайте сначала определим поля GridView. В смарт-теге GridView щелкните ссылку "Изменить столбцы", чтобы открыть диалоговое окно Поля (см. рис. 6). Здесь снимите флажок "Автоматическое создание полей" в левом нижнем углу. Так как мы хотим, чтобы этот элемент GridView включал возможности редактирования и удаления, добавьте CommandField и задайте для него ShowEditButton свойства и ShowDeleteButton значение True. Затем добавьте четыре поля для отображения UserNameсвойств , Email, LastLoginDateи Comment . Используйте BoundField для двух доступных только для чтения свойств (UserName и LastLoginDate) и TemplateFields для двух редактируемых полей (Email и Comment).

Сначала в BoundField отобразится UserName свойство ; задайте для его HeaderText свойств и DataField значение UserName. Это поле не будет изменяться, поэтому задайте для его ReadOnly свойства значение True. Настройте BoundField, задав для нее LastLoginDateHeaderText значение "Last Login", а для параметра DataField — значение LastLoginDate. Давайте отформатируем выходные данные этого BoundField так, чтобы отображалась только дата (вместо даты и времени). Для этого задайте для свойства BoundField HtmlEncode значение False, а для свойства DataFormatString — значение "{0:d}". Также присвойте свойству ReadOnly значение True.

HeaderText Задайте для свойств двух templateFields значения "Email" и "Комментарий".

Поля GridView можно настроить с помощью диалогового окна

Рис. 6. Поля GridView можно настроить с помощью диалогового окна Поля (щелкните для просмотра полноразмерного изображения)

Теперь необходимо определить ItemTemplate и EditItemTemplate для шаблонов TemplateField Email и Comment. Добавьте веб-элемент управления Label в каждый элемент ItemTemplate управления и привяжите их Text свойства к свойствам Email и Comment соответственно.

Для Email TemplateField добавьте элемент TextBox с именем EmailEditItemTemplate и привяжите его Text свойство к свойству Email с помощью двусторонней привязки данных. Добавьте RequiredFieldValidator и RegularExpressionValidator в EditItemTemplate , чтобы убедиться, что посетитель, изменяющий свойство Email, ввел допустимый адрес электронной почты. Для поля TemplateField "Comment" добавьте многострочный элемент TextBox с именем Comment в .EditItemTemplate Задайте для свойств TextBox Columns и Rows значения 40 и 4 соответственно, а затем привяжите его Text свойство к свойству Comment с помощью двусторонней привязки данных.

После настройки шаблонов их декларативная разметка должна выглядеть примерно так:

<asp:TemplateField HeaderText="Email">    
     <ItemTemplate>    
          <asp:Label runat="server" ID="Label1" Text='<%# Eval("Email") %>'></asp:Label>    

     </ItemTemplate>    
     <EditItemTemplate>    
          <asp:TextBox runat="server" ID="Email" Text='<%# Bind("Email") %>'></asp:TextBox>    

          <asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server"    
               ControlToValidate="Email" Display="Dynamic"    
               ErrorMessage="You must provide an email address." 
               SetFocusOnError="True">*</asp:RequiredFieldValidator>    

          <asp:RegularExpressionValidator ID="RegularExpressionValidator1" runat="server"    
               ControlToValidate="Email" Display="Dynamic"    
               ErrorMessage="The email address you have entered is not valid. Please fix 
               this and try again."    
               SetFocusOnError="True"    

               ValidationExpression="\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*">*
          </asp:RegularExpressionValidator>    
     </EditItemTemplate>    
</asp:TemplateField>

<asp:TemplateField HeaderText="Comment">    
     <ItemTemplate>    
          <asp:Label runat="server" ID="Label2" Text='<%# Eval("Comment") %>'></asp:Label>    

     </ItemTemplate>    
     <EditItemTemplate>    
          <asp:TextBox runat="server" ID="Comment" TextMode="MultiLine"
               Columns="40" Rows="4" Text='<%# Bind("Comment") %>'>

          </asp:TextBox>    
     </EditItemTemplate>    
</asp:TemplateField>

При изменении или удалении учетной записи пользователя необходимо знать значение свойства этого пользователя UserName . Задайте для свойства GridView DataKeyNames значение UserName, чтобы эти сведения были доступны в коллекции GridView DataKeys .

Наконец, добавьте элемент управления ValidationSummary на страницу и задайте для его ShowMessageBox свойства значение True, а для свойства ShowSummary — значение False. При использовании этих параметров ValidationSummary будет отображаться оповещение на стороне клиента, если пользователь пытается изменить учетную запись пользователя с отсутствующим или недопустимым адресом электронной почты.

<asp:ValidationSummary ID="ValidationSummary1"
               runat="server"
               ShowMessageBox="True"
               ShowSummary="False" />

Теперь мы завершили декларативную разметку этой страницы. Следующая задача — привязать набор учетных записей пользователей к GridView. Добавьте метод с именем BindUserGrid в RoleBasedAuthorization.aspx класс кода программной части страницы, который привязывает MembershipUserCollection возвращаемый класс Membership.GetAllUsers к UserGrid GridView. Вызовите этот метод из обработчика Page_Load событий при первом посещении страницы.

protected void Page_Load(object sender, EventArgs e)    
{    
     if (!Page.IsPostBack)    
          BindUserGrid();    
}

private void BindUserGrid()    
{    
     MembershipUserCollection allUsers = Membership.GetAllUsers();    
     UserGrid.DataSource = allUsers;    
     UserGrid.DataBind();    
}

После этого кода перейдите на страницу через браузер. Как показано на рисунке 7, вы увидите GridView со списком сведений о каждой учетной записи пользователя в системе.

Элемент UserGrid GridView выводит сведения о каждом пользователе в системе.

Рис. 7. GridView UserGrid содержит сведения о каждом пользователе в системе (щелкните для просмотра полноразмерного изображения)

Примечание

GridView UserGrid выводит список всех пользователей в интерфейсе без страниц. Этот простой интерфейс сетки не подходит для сценариев, в которых есть несколько десятков или более пользователей. Один из вариантов — настроить GridView для включения разбиения по страницам. Метод Membership.GetAllUsers имеет две перегрузки: одна, которая не принимает входные параметры и возвращает всех пользователей, а другая принимает целочисленные значения для индекса страницы и размера страницы и возвращает только указанное подмножество пользователей. Вторая перегрузка может использоваться для более эффективного перебора пользователей, так как она возвращает только точное подмножество учетных записей пользователей, а не все из них. Если у вас тысячи учетных записей пользователей, можно рассмотреть интерфейс на основе фильтра, который отображает только тех пользователей, имя пользователя которых начинается с выбранного символа. идеально Membership.FindUsersByName method подходит для создания пользовательского интерфейса на основе фильтров. Мы рассмотрим создание такого интерфейса в следующем руководстве.

Элемент управления GridView обеспечивает встроенную поддержку редактирования и удаления, если элемент управления привязан к правильно настроенной системе управления источником данных, например SqlDataSource или ObjectDataSource. GridView UserGrid , однако, имеет программную привязку данных, поэтому для выполнения этих двух задач необходимо написать код. В частности, необходимо создать обработчики событий GridView RowEditing, RowCancelingEdit, RowUpdatingи RowDeleting , которые активируются, когда посетитель нажимает кнопки Изменить, Отмена, Обновить или Удалить GridView.

Начните с создания обработчиков событий для событий GridView RowEditing, RowCancelingEditи , а RowUpdating затем добавьте следующий код:

protected void UserGrid_RowEditing(object sender, GridViewEditEventArgs e)
{
     // Set the grid's EditIndex and rebind the data

     UserGrid.EditIndex = e.NewEditIndex;
     BindUserGrid();
}

protected void UserGrid_RowCancelingEdit(object sender, GridViewCancelEditEventArgs e)
{
     // Revert the grid's EditIndex to -1 and rebind the data
     UserGrid.EditIndex = -1;
     BindUserGrid();
}

protected void UserGrid_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
     // Exit if the page is not valid
     if (!Page.IsValid)
          return;

     // Determine the username of the user we are editing
     string UserName = UserGrid.DataKeys[e.RowIndex].Value.ToString();

     // Read in the entered information and update the user
     TextBox EmailTextBox = UserGrid.Rows[e.RowIndex].FindControl("Email") as TextBox;
     TextBox CommentTextBox = UserGrid.Rows[e.RowIndex].FindControl("Comment") as TextBox;

     // Return information about the user
     MembershipUser UserInfo = Membership.GetUser(UserName);

     // Update the User account information
     UserInfo.Email = EmailTextBox.Text.Trim();
     UserInfo.Comment = CommentTextBox.Text.Trim();

     Membership.UpdateUser(UserInfo);

     // Revert the grid's EditIndex to -1 and rebind the data
     UserGrid.EditIndex = -1;
     BindUserGrid();
}

RowEditing Обработчики событий и RowCancelingEdit просто задают свойство GridViewEditIndex, а затем повторно привязывал список учетных записей пользователей к сетке. Интересные вещи происходят в обработчике RowUpdating событий. Этот обработчик событий начинается с проверки допустимости данных, а затем извлекает UserName значение измененной учетной записи пользователя из DataKeys коллекции. Затем Email программными ссылками на и Comment TextBoxes в двух templateFields EditItemTemplate . Их Text свойства содержат измененный адрес электронной почты и комментарий.

Чтобы обновить учетную запись пользователя с помощью API членства, необходимо сначала получить сведения о пользователе, что мы делаем через вызов Membership.GetUser(userName). Затем свойства возвращаемого MembershipUserEmail объекта и Comment обновляются значениями, введенными в два элемента TextBox из интерфейса редактирования. Наконец, эти изменения сохраняются с помощью вызова Membership.UpdateUser. Обработчик RowUpdating событий завершает работу, возвращая GridView к интерфейсу предварительного редактирования.

Затем создайте RowDeleting обработчик событий и добавьте следующий код:

protected void UserGrid_RowDeleting(object sender, GridViewDeleteEventArgs e)
{
     // Determine the username of the user we are editing
     string UserName = UserGrid.DataKeys[e.RowIndex].Value.ToString();

     // Delete the user
     Membership.DeleteUser(UserName);

     // Revert the grid's EditIndex to -1 and rebind the data
     UserGrid.EditIndex = -1;
     BindUserGrid();
}

Приведенный выше обработчик событий начинается с захвата UserName значения из коллекции GridViewDataKeys. Затем это UserName значение передается в метод класса DeleteUserMembership. Метод DeleteUser удаляет учетную запись пользователя из системы, включая связанные данные членства (например, к каким ролям принадлежит этот пользователь). После удаления пользователя для сетки EditIndex устанавливается значение -1 (если пользователь нажал удалить, когда другая строка находилась в режиме BindUserGrid редактирования), и вызывается метод .

Примечание

Кнопка Удалить не требует подтверждения от пользователя перед удалением учетной записи пользователя. Я призываю вас добавить некоторую форму подтверждения пользователя, чтобы уменьшить вероятность случайного удаления учетной записи. Одним из самых простых способов подтверждения действия является диалоговое окно подтверждения на стороне клиента. Дополнительные сведения об этом методе см. в разделе Добавление подтверждения Client-Side при удалении.

Убедитесь, что эта страница работает должным образом. Вы сможете изменить адрес электронной почты и комментарий любого пользователя, а также удалить любую учетную запись пользователя. Так как страница RoleBasedAuthorization.aspx доступна для всех пользователей, любой пользователь , даже анонимный посетители, может посетить эту страницу, а также изменить и удалить учетные записи пользователей! Давайте обновим эту страницу так, чтобы только пользователи с ролями "Супервизоры" и "Администраторы" могли изменять адрес электронной почты и комментарий пользователя, а только администраторы могут удалять учетную запись пользователя.

В разделе "Использование элемента управления LoginView" рассматривается использование элемента управления LoginView для отображения инструкций, относящихся к роли пользователя. Если пользователь с ролью "Администраторы" заходит на эту страницу, мы отобразим инструкции по изменению и удалению пользователей. Если пользователь с ролью "Супервизоры" достигает этой страницы, мы отобразим инструкции по редактированию пользователей. А если посетитель является анонимным или не имеет роли "Супервизоры" или "Администраторы", мы отобразим сообщение о том, что он не может изменять или удалять сведения об учетной записи пользователя. В разделе "Программное ограничение функциональности" мы напишем код, который программным способом отображает или скрывает кнопки Изменить и Удалить в зависимости от роли пользователя.

Использование элемента управления LoginView

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

Начните с добавления LoginView над UserGrid GridView. Как мы уже говорили ранее, элемент управления LoginView имеет два встроенных шаблона: AnonymousTemplate и LoggedInTemplate. Введите краткое сообщение в обоих шаблонах, информирующее пользователя о том, что он не может изменять или удалять какие-либо сведения о пользователе.

<asp:LoginView ID="LoginView1" runat="server">
     <LoggedInTemplate>
          You are not a member of the Supervisors or Administrators roles. Therefore you
          cannot edit or delete any user information.
     </LoggedInTemplate>
     <AnonymousTemplate>
          You are not logged into the system. Therefore you cannot edit or delete any user

          information.
     </AnonymousTemplate>
</asp:LoginView>

В дополнение к AnonymousTemplate и LoggedInTemplateэлемент управления LoginView может включать RoleGroups, которые являются шаблонами для конкретных ролей. Каждая группа RoleGroup содержит одно свойство , которое указывает, Rolesк каким ролям применяется RoleGroup. Для Roles свойства можно задать одну роль (например, "Администраторы") или список ролей с разделителями-запятыми (например, "Администраторы, руководители").

Чтобы управлять группами RoleGroups, щелкните ссылку Edit RoleGroups (Изменить группы ролей) в смарт-теге элемента управления, чтобы открыть редактор коллекции RoleGroup. Добавьте две новые группы ролей. Задайте для первого свойства RoleGroup Roles значение "Администраторы", а для второго — значение "Супервизоры".

Управление шаблонами Role-Specific LoginView с помощью редактора коллекции RoleGroup

Рис. 8. Управление шаблонами Role-Specific LoginView с помощью редактора коллекций RoleGroup (щелкните для просмотра полноразмерного изображения)

Нажмите кнопку ОК, чтобы закрыть редактор коллекции RoleGroup; Обновляет декларативную разметку LoginView, включив раздел с дочерним элементом <RoleGroups><asp:RoleGroup> для каждой группы RoleGroup, определенной в редакторе коллекции RoleGroup. Кроме того, раскрывающийся список "Представления" в смарт-теге LoginView, который изначально перечислял только AnonymousTemplate и LoggedInTemplate , теперь также включает добавленные группы RoleGroups.

Измените roleGroups, чтобы пользователи в роли "Супервизоры" отображали инструкции по изменению учетных записей пользователей, а пользователи в роли "Администраторы" — инструкции по редактированию и удалению. После внесения этих изменений декларативная разметка LoginView должна выглядеть примерно так:

<asp:LoginView ID="LoginView1" runat="server">
     <RoleGroups>
          <asp:RoleGroup Roles="Administrators">
               <ContentTemplate>
                    As an Administrator, you may edit and delete user accounts. 
                    Remember: With great power comes great responsibility!

               </ContentTemplate>
          </asp:RoleGroup>
          <asp:RoleGroup Roles="Supervisors">
               <ContentTemplate>
                    As a Supervisor, you may edit users&#39; Email and Comment information. 
                    Simply click the Edit button, make your changes, and then click Update.
               </ContentTemplate>
          </asp:RoleGroup>
     </RoleGroups>

     <LoggedInTemplate>
          You are not a member of the Supervisors or Administrators roles. 
          Therefore you cannot edit or delete any user information.
     </LoggedInTemplate>
     <AnonymousTemplate>
          You are not logged into the system. Therefore you cannot edit or delete any user
          information.
     </AnonymousTemplate>
</asp:LoginView>

После внесения этих изменений сохраните страницу и посетите ее в браузере. Сначала посетите страницу в качестве анонимного пользователя. Должно появиться сообщение "Вы не вошли в систему. Поэтому вы не можете изменять или удалять какие-либо сведения о пользователе". Затем войдите в систему в качестве пользователя, прошедшего проверку подлинности, но не с ролью "Супервизоры" или "Администраторы". На этот раз должно появиться сообщение "Вы не являетесь членом ролей "Супервизоры" или "Администраторы". Поэтому вы не можете изменять или удалять какие-либо сведения о пользователе".

Затем войдите в систему как пользователь, который является членом роли "Супервизоры". На этот раз вы увидите сообщение о роли "Супервизоры" (см. рис. 9). А если вы входите в систему как пользователь с ролью Администраторы, вы увидите сообщение о конкретной роли администраторов (см. рис. 10).

Брюсу отображается сообщение Role-Specific руководителей

Рис. 9. Брюс отображает сообщение Role-Specific руководителей (щелкните, чтобы просмотреть полноразмерное изображение)

Тито отображается сообщение

Рис. 10. Тито отображается сообщение "Администраторы Role-Specific" (щелкните для просмотра полноразмерного изображения)

Как показано на снимках экрана на рис. 9 и 10, LoginView отображает только один шаблон, даже если применяется несколько шаблонов. Брюс и Тито являются пользователями, вошедшего в систему, но LoginView отображает только соответствующую группу RoleGroup, а не LoggedInTemplate. Кроме того, Тито принадлежит к ролям "Администраторы" и "Супервизоры", но элемент управления LoginView отображает шаблон для конкретной роли "Администраторы", а не "Супервизоры".

На рисунке 11 показан рабочий процесс, используемый элементом управления LoginView для определения шаблона для отрисовки. Обратите внимание, что если указано несколько групп RoleGroup, шаблон LoginView отрисовывает первую соответствующую группу RoleGroup. Иными словами, если бы мы поместили группу ролей руководителей в качестве первой, а администраторы — в качестве второй, то, когда Тито посетил эту страницу, он увидит сообщение "Руководители".

Рабочий процесс элемента управления LoginView для определения шаблона для отображения

Рис. 11. Рабочий процесс элемента управления LoginView для определения шаблона для отображения (щелкните для просмотра полноразмерного изображения)

Программное ограничение функциональности

Хотя элемент управления LoginView отображает различные инструкции в зависимости от роли пользователя, посещающего страницу, кнопки Изменить и Отмена остаются видимыми для всех пользователей. Нам нужно программно скрыть кнопки Правка и Удалить для анонимных посетителей и пользователей, которые не имеют ни роли руководителей, ни администраторов. Нам нужно скрыть кнопку Удалить для всех, кто не является администратором. Для этого мы напишем небольшой код, который программным способом ссылается на элементы CommandField's Edit и Delete LinkButtons и при необходимости задает их Visible свойствам falseзначение .

Самый простой способ программной ссылки на элементы управления в CommandField — сначала преобразовать их в шаблон. Для этого щелкните ссылку "Изменить столбцы" в смарт-теге GridView, выберите CommandField из списка текущих полей и щелкните ссылку "Преобразовать это поле в TemplateField". Это преобразует CommandField в TemplateField с и ItemTemplateEditItemTemplate. Содержит ItemTemplate элементы Edit и Delete LinkButtons, а в ней EditItemTemplate размещаются элементы Update и Cancel LinkButtons.

Преобразование CommandField в templateField

Рис. 12. Преобразование CommandField в TemplateField (щелкните, чтобы просмотреть полноразмерное изображение)

Обновите кнопки Edit и Delete LinkButtons в ItemTemplate, установив для их ID свойств значения EditButton и DeleteButtonсоответственно.

<asp:TemplateField ShowHeader="False">
     <EditItemTemplate>
          <asp:LinkButton ID="LinkButton1" runat="server" CausesValidation="True"
               CommandName="Update" Text="Update"></asp:LinkButton>

           <asp:LinkButton ID="LinkButton2" runat="server" CausesValidation="False"
                CommandName="Cancel" Text="Cancel"></asp:LinkButton>

     </EditItemTemplate>
     <ItemTemplate>
          <asp:LinkButton ID="EditButton" runat="server" CausesValidation="False"
               CommandName="Edit" Text="Edit"></asp:LinkButton>

           <asp:LinkButton ID="DeleteButton" runat="server" CausesValidation="False"
               CommandName="Delete" Text="Delete"></asp:LinkButton>

     </ItemTemplate>
</asp:TemplateField>

Всякий раз, когда данные привязаны к GridView, GridView перечисляет записи в своем DataSource свойстве и создает соответствующий GridViewRow объект. При создании RowCreated каждого GridViewRow объекта запускается событие . Чтобы скрыть кнопки "Изменить" и "Удалить" для неавторизованных пользователей, необходимо создать обработчик событий для этого события и программно сослаться на кнопки Edit и Delete LinkButtons, задав соответствующие Visible свойства.

Создайте обработчик события и RowCreated добавьте следующий код:

protected void UserGrid_RowCreated(object sender, GridViewRowEventArgs e)
{
     if (e.Row.RowType == DataControlRowType.DataRow && e.Row.RowIndex != UserGrid.EditIndex)
     {
          // Programmatically reference the Edit and Delete LinkButtons
          LinkButton EditButton = e.Row.FindControl("EditButton") as LinkButton;

          LinkButton DeleteButton = e.Row.FindControl("DeleteButton") as LinkButton;

          EditButton.Visible = (User.IsInRole("Administrators") || User.IsInRole("Supervisors"));
          DeleteButton.Visible = User.IsInRole("Administrators");
     }
}

Помните, что RowCreated событие срабатывает для всех строк GridView, включая верхний колонтитул, нижний колонтитул, интерфейс пейджера и т. д. Мы хотим программно ссылаться на кнопки Edit и Delete LinkButtons, только если мы имеем дело со строкой данных, не в режиме редактирования (так как в строке в режиме редактирования вместо правки есть кнопки "Обновить" и "Отмена", а не "Изменить" и "Удалить"). Эта проверка обрабатывается оператором if .

Если мы имеем дело со строкой данных, которая не находится в режиме редактирования, ссылки на элементы Edit и Delete LinkButtons задаются, а их Visible свойства задаются на основе логических значений, возвращаемых методом User объекта IsInRole(roleName) . Объект User ссылается на субъект, созданный с помощью RoleManagerModule; следовательно, IsInRole(roleName) метод использует API ролей, чтобы определить, принадлежит ли текущий посетитель к roleName.

Примечание

Мы могли бы использовать класс Roles напрямую, заменив вызов User.IsInRole(roleName) на вызов Roles.IsUserInRole(roleName) метода . В этом примере я решил использовать метод основного IsInRole(roleName) объекта, так как он более эффективен, чем использование API ролей напрямую. Ранее в этом руководстве мы настроили диспетчер ролей для кэширования ролей пользователя в файле cookie. Эти кэшированные данные файлов cookie используются только при вызове метода субъекта IsInRole(roleName) . Прямые вызовы API ролей всегда связаны с поездкой в хранилище ролей. Даже если роли не кэшируются в файле cookie, вызов метода основного объекта IsInRole(roleName) обычно более эффективен, так как при первом вызове во время запроса он кэширует результаты. API ролей, с другой стороны, не выполняет кэширование. RowCreated Так как событие запускается один раз для каждой строки в GridView, использование User.IsInRole(roleName) включает только одну поездку в хранилище ролей, тогда как Roles.IsUserInRole(roleName) требует N поездок, где N — количество учетных записей пользователей, отображаемых в сетке.

Свойству кнопки Visible Изменить присваивается значение true , если пользователь, посещая эту страницу, имеет роль администраторов или руководителей; в противном случае ему присваивается значение false. Свойству кнопки Visible Удалить присваивается значение true только в том случае, если пользователь находится в роли Администраторы.

Проверьте эту страницу в браузере. Если вы посещаете страницу как анонимный посетитель или как пользователь, который не является ни руководителем, ни администратором, commandField будет пустым; он по-прежнему существует, но в виде тонкой щепки без кнопок "Изменить" или "Удалить".

Примечание

Можно полностью скрыть CommandField, если страницу посещает не руководитель и администратор. Я оставляю это как упражнение для читателя.

Кнопки

Рис. 13. Кнопки "Изменить" и "Удалить" скрыты для пользователей, не являющихся руководителями и неадминистраторами (щелкните для просмотра полноразмерного изображения)

Если пользователь, принадлежащий к роли "Супервизоры" (но не к роли "Администраторы"), посещает его, он видит только кнопку Изменить.

Хотя кнопка

Рис. 14. Хотя кнопка "Изменить" доступна для руководителей, кнопка "Удалить" скрыта (щелкните, чтобы просмотреть полноразмерное изображение)

А если администратор посещает, у него есть доступ к кнопкам Изменить и Удалить.

Кнопки

Рис. 15. Кнопки "Изменить" и "Удалить" доступны только для администраторов (щелкните для просмотра полноразмерного изображения)

Шаг 3. Применение правил авторизации Role-Based к классам и методам

На шаге 2 мы ограничили возможности редактирования пользователями с ролями "Супервизоры" и "Администраторы" и удаляем возможности только для администраторов. Это было достигнуто путем скрытия связанных элементов пользовательского интерфейса для неавторизованных пользователей с помощью программных методов. Такие меры не гарантируют, что несанкционированный пользователь не сможет выполнить привилегированное действие. Элементы пользовательского интерфейса могут быть добавлены позже или которые мы забыли скрыть для неавторизованных пользователей. Или злоумышленник может обнаружить другой способ получить страницу ASP.NET для выполнения нужного метода.

Простой способ убедиться, что неавторизованный пользователь не сможет получить доступ к определенной части функциональности, — украсить этот класс или метод атрибутомPrincipalPermission . Когда среда выполнения .NET использует класс или выполняет один из своих методов, она проверяет, имеет ли текущий контекст безопасности разрешение. Атрибут PrincipalPermission предоставляет механизм, с помощью которого можно определить эти правила.

Мы рассмотрели использование атрибута PrincipalPermission в руководстве по авторизации на основе пользователя. В частности, мы узнали, как украсить обработчик событий GridView SelectedIndexChanged и RowDeleting так, чтобы они могли выполняться только пользователями, прошедшими проверку подлинности, и Tito соответственно. Атрибут PrincipalPermission также работает с ролями.

Давайте продемонстрируем использование атрибута PrincipalPermission в обработчиках событий GridView RowUpdating и RowDeleting , чтобы запретить выполнение для неавторизоваемых пользователей. Все, что нам нужно сделать, это добавить соответствующий атрибут в каждое определение функции:

[PrincipalPermission(SecurityAction.Demand, Role = "Administrators")]
[PrincipalPermission(SecurityAction.Demand, Role = "Supervisors")]
protected void UserGrid_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
     ...
}

[PrincipalPermission(SecurityAction.Demand, Role = "Administrators")]
protected void UserGrid_RowDeleting(object sender, GridViewDeleteEventArgs e)
{
     ...
}

Атрибут обработчика RowUpdating событий определяет, что обработчик событий может выполнять только пользователи с ролями "Администраторы" или "Супервизоры", где в качестве атрибута RowDeleting в обработчике событий выполнение ограничивается пользователями с ролью "Администраторы".

Примечание

Атрибут PrincipalPermission представлен в виде класса в System.Security.Permissions пространстве имен . Не забудьте добавить using System.Security.Permissions оператор в начало файла класса кода программной части, чтобы импортировать это пространство имен.

Если каким-либо образом неадминистратор пытается выполнить RowDeleting обработчик событий или если обработчик событий не является руководителем или администратором RowUpdating , среда выполнения .NET вызовет SecurityException.

Если контекст безопасности не авторизован для выполнения метода, создается исключение SecurityException.

Рис. 16. Если контекст безопасности не авторизован для выполнения метода, SecurityException возникает исключение (щелкните, чтобы просмотреть полноразмерное изображение)

Помимо ASP.NET страниц, многие приложения также имеют архитектуру, которая включает различные слои, такие как бизнес-логика и уровни доступа к данным. Эти уровни обычно реализуются как библиотеки классов и предлагают классы и методы для выполнения функциональных возможностей, связанных с бизнес-логикой и данными. Атрибут PrincipalPermission также полезен для применения правил авторизации к этим уровням.

Дополнительные сведения об использовании атрибута PrincipalPermission для определения правил авторизации для классов и методов см. в записи блога Скотта ГатриДобавление правил авторизации в уровни бизнеса и данных с помощью PrincipalPermissionAttributes.

Сводка

В этом руководстве мы рассмотрели, как указать грубые и детализированные правила авторизации на основе ролей пользователя. ASP. Функция авторизации URL-адресов NET позволяет разработчику страниц указать, какие удостоверения разрешен или запрещен доступ к тем или иным страницам. Как мы уже видели в руководстве по авторизации на основе пользователя, правила авторизации URL-адресов можно применять для каждого пользователя. Их также можно применять на основе ролей, как показано на шаге 1 этого руководства.

Правила детальной авторизации могут применяться декларативно или программно. На шаге 2 мы рассмотрели использование функции RoleGroups элемента управления LoginView для отображения различных выходных данных в зависимости от ролей посещающих пользователей. Мы также рассмотрели способы программного определения того, принадлежит ли пользователь определенной роли и как соответствующим образом настроить функциональность страницы.

Счастливое программирование!

Дополнительные материалы

Дополнительные сведения по темам, рассматриваемым в этом руководстве, см. в следующих ресурсах:

Об авторе

Скотт Митчелл (Scott Mitchell), автор нескольких книг ASP/ASP.NET и основатель 4GuysFromRolla.com, работает с веб-технологиями Майкрософт с 1998 года. Скотт работает независимым консультантом, тренером и писателем. Его последняя книга Sams Teach Yourself ASP.NET 2.0 в 24 часа. Скотт можно связаться по адресу mitchell@4guysfromrolla.com или через его блог по адресу http://ScottOnWriting.NET.

Отдельная благодарность...

Эта серия учебников была проверена многими полезными рецензентами. К ведущим рецензентам этого руководства относятся Сучи Банерджи и Тетера Мерфи. Хотите ознакомиться с моими предстоящими статьями MSDN? Если да, бросить мне линию на mitchell@4GuysFromRolla.com