Главная страница   /   11.1. Безопасность административной части (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

11.1. Безопасность административной части

Поскольку ASP.NET MVC построена на ядре платформы ASP.NET, у нас есть доступ к forms-аутентификации, которая представляет собой универсальную систему для отслеживания входа в приложение. В нашем примере мы просто покажем вам, как провести базовую настройку.

Если вы откроете файл Web.config (в корневой папке проекта), вы сможете найти раздел authentication, который выглядит так:

<authentication mode="Forms">
	<forms loginUrl="~/Account/Login" timeout="2880"/>
</authentication>

Как видите, forms-аутентификация автоматически включена в приложении MVC, созданном на шаблоне Basic. Атрибут loginUrl сообщает ASP.NET, по какому URL нужно перенаправить пользователей для аутентификации - в данном случае, это страница ~/Account/Login. Атрибут timeout определяет, как долго пользователь остается аутентифицированным после входа в систему. По умолчанию, это 48 часов (2880 минут).

Примечание

Основной альтернативой forms-аутентификации является Windows-аутентификация, которая использует учетные данные операционной системы для идентификации пользователей. Это отличное средство, если вы развертываете интранет-приложения и все ваши пользователи находятся в одном домене Windows, но оно не подходит для интернет-приложений.

Если вы создаете проект приложения MVC, используя опции Internet Application или Mobile Application, Visual Studio автоматически создаст класс AccountController, который будет обрабатывать запросы на аутентификацию с помощью функции членства (membership) ASP.NET.

При создании проекта SportStore.WebUI мы выбрали опцию Basic, что означает, что у нас включена forms-аутентификация в файле Web.config, но нужно создать контроллер, который будет проводить аутентификацию. Это означает, что мы можем реализовать любую модель аутентификации, что очень кстати - функция членства ASP.NET будет излишеством для нашего примера, и для знакомства с функциями безопасности MVC будет достаточно более простой аутентификации.

Для начала мы создадим имя пользователя и пароль, которые будут предоставлять доступ к функциям администрирования SportsStore. В листинге 11-1 показаны изменения, которые мы внесли в раздел аутентификации файла Web.config.

Листинг 11-1: Определяем имя пользователя и пароль
<authentication mode="Forms">
	<forms loginUrl="~/Account/Login" timeout="2880">
		<credentials passwordFormat="Clear">
			<user name="admin" password="secret" />
		</credentials>
	</forms>
</authentication>

Мы не стали усложнять процесс и жестко закодировали имя пользователя (admin) и пароль (secret) в файле Web.config. В этой главе мы хотим сфокусироваться на обеспечении базовой безопасности приложения MVC, так что жесткое кодирование учетных данных нам подойдет.

Внимание!

Естественно, мы не рекомендуем развертывать реальное приложение с таким базовым уровнем безопасности. Здесь мы только хотим показать вам основные функции безопасности MVC Framework. Статические и жестко закодированные пароли не пригодны для реальных проектов.

Применяем фильтры авторизации

Фильтры – это очень мощная функция MVC Framework. Они представляют собой атрибуты .NET, которые можно применить к методу действия или классу контроллера. Они предоставляют дополнительную логику при обработке запроса. Доступны различные виды фильтров, и вы даже можете создавать пользовательские фильтры, что мы рассмотрим в главе 16. Фильтр, который интересует нас в данный момент – это стандартный фильтр авторизации Authorize. Мы применим его к классу AdminController, как показано в листинге 11-2.

Листинг 11-2: Добавляем атрибут Authorize к классу Controller
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace SportsStore.WebUI.Controllers
{
	[Authorize]
	public class AdminController : Controller
	{
		private IProductRepository repository;
		public AdminController(IProductRepository repo)
		{
			repository = repo;
		}
		// ...action methods omitted for brevity...
	}
}

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

Примечание

Можно применять фильтры к отдельным методам действия или целому контроллеру. При применении фильтра к контроллеру он будет работать так, как если бы вы применили его к каждому методу действия в классе контроллера. В листинге 11-2 мы применяем фильтр Authorize к целому классу, так что все методы действий контроллера Admin доступны только авторизованным пользователям.

Чтобы увидеть эффект фильтра Authorize, запустите приложение и перейдите по ссылке /Admin/Index. Вы увидите ошибку, как показано на рисунке 11-1.

Рисунок 11-1: Эффект фильтра Authorize

Если вы попытаетесь получить доступ к методу действия Index контроллера Admin, MVC Framework обнаружит фильтр Authorize. Так как вы не прошли аутентификацию, вы будете перенаправлены на адрес, заданный в разделе forms-аутентификации файла Web.config: /Account/Login. Мы еще не создали контроллер Account, и это приводит к ошибке, показанной на рисунке. Однако тот факт, что платформа MVC пыталась создать экземпляр класса AccountController, указывает на то, что атрибут Authorize работает.

Создаем провайдер аутентификации

Для использования forms-аутентификации потребуется вызвать два статических метода класса System.Web.Security.FormsAuthentication:

  • Метод Authenticate, который позволяет нам провести валидацию предоставленных пользователем учетных данных.
  • Метод SetAuthCookie, который добавляет cookie в ответ браузеру, так что пользователю не придется проходить аутентификацию при отправке каждого нового запроса.

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

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

Мы начнем с определения интерфейса провайдера аутентификации. Создайте новую папку под названием Abstract в папке Infrastructure проекта SportsStore.WebUI и добавьте в нее новый интерфейс под названием IAuthProvider. Его содержимое показано в листинге 11-3.

Листинг 11-3: Интерфейс IAuthProvider
namespace SportsStore.WebUI.Infrastructure.Abstract {
	public interface IAuthProvider {
		bool Authenticate(string username, string password);
	}
}

Теперь мы можем создать реализацию этого интерфейса, которая будет служить оболочкой для статических методов класса FormsAuthentication. Создайте еще одну папку в папке Infrastructure, назовите ее Concrete и определите в ней новый класс под названием FormsAuthProvider. Содержание этого класса показано в листинге 11-4.

Листинг 11-4: Класс FormsAuthProvider
using System.Web.Security;
using SportsStore.WebUI.Infrastructure.Abstract;
namespace SportsStore.WebUI.Infrastructure.Concrete
{
	public class FormsAuthProvider : IAuthProvider
	{
		public bool Authenticate(string username, string password)
		{
			bool result = FormsAuthentication.Authenticate(username, password);
			if (result)
			{
				FormsAuthentication.SetAuthCookie(username, false);
			}
			return result;
		}
	}
}

Реализация метода Authenticate вызывает статические методы FormsAuthentication, которые мы хотели держать отдельно от контроллера. Последним шагом будет регистрация FormsAuthProvider в методе AddBindings класса NinjectControllerFactory, как показано в листинге 11-5 (изменения выделены жирным шрифтом).

Листинг 11-5: Добавляем привязку Ninject для провайдера аутентификации
using System;
using System.Web.Mvc;
using System.Web.Routing;
using Ninject;
using SportsStore.Domain.Entities;
using SportsStore.Domain.Abstract;
using System.Collections.Generic;
using System.Linq;
using Moq;
using SportsStore.Domain.Concrete;
using System.Configuration;
using SportsStore.WebUI.Infrastructure.Abstract;
using SportsStore.WebUI.Infrastructure.Concrete;
namespace SportsStore.WebUI.Infrastructure
{
	public class NinjectControllerFactory : DefaultControllerFactory
	{
		private IKernel ninjectKernel;
		public NinjectControllerFactory()
		{
			ninjectKernel = new StandardKernel();
			AddBindings();
		}
		protected override IController GetControllerInstance(RequestContext
		requestContext, Type controllerType)
		{
			return controllerType == null
				? null
				: (IController)ninjectKernel.Get(controllerType);
		}
		private void AddBindings()
		{
			ninjectKernel.Bind<IProductRepository>().To<EFProductRepository>();
			EmailSettings emailSettings = new EmailSettings
			{
				WriteAsFile = bool.Parse(ConfigurationManager
				.AppSettings["Email.WriteAsFile"] ?? "false")
			};
			ninjectKernel.Bind<IOrderProcessor>()
				.To<EmailOrderProcessor>()
				.WithConstructorArgument("settings", emailSettings);
			ninjectKernel.Bind<IAuthProvider>().To<FormsAuthProvider>();
		}
	}
}

Создаем контроллер Account

Наша следующая задача – создать контроллер Account и метод действия Login, упомянутые в файле Web.config. На самом деле, мы создадим две версии метода Login. Первая будет визуализировать представление, которое содержит подсказку для входа в систему, а другая будет обрабатывать запрос POST, после того как пользователь отправит свои учетные данные.

Для начала мы создадим класс модели представления, который будем передавать между контроллером и представлением. Добавьте в папку Models проекта SportsStore.WebUI новый класс под названием LoginViewModel и приведите его содержание в соответствие с листингом 11-6.

Листинг 11-6: Класс LoginViewModel
using System.ComponentModel.DataAnnotations;
namespace SportsStore.WebUI.Models
{
	public class LoginViewModel
	{
		[Required]
		public string UserName { get; set; }
		[Required]
		[DataType(DataType.Password)]
		public string Password { get; set; }
	}
}

Этот класс содержит свойства для имени пользователя и пароля, а также использует атрибуты DataAnnotation, чтобы указать, что требуются значения обоих свойств. Кроме того, мы используем атрибут DataType, чтобы сообщить MVC Framework, как мы хотим отображать редактор свойства Password.

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

Далее создайте новый контроллер под названием AccountController, как показано в листинге 11-7.

Листинг 11-7: Класс AccountController
using System.Web.Mvc;
using SportsStore.WebUI.Infrastructure.Abstract;
using SportsStore.WebUI.Models;
namespace SportsStore.WebUI.Controllers
{
	public class AccountController : Controller
	{
		IAuthProvider authProvider;
		public AccountController(IAuthProvider auth)
		{
			authProvider = auth;
		}
		public ViewResult Login()
		{
			return View();
		}
		[HttpPost]
		public ActionResult Login(LoginViewModel model, string returnUrl)
		{
			if (ModelState.IsValid)
			{
				if (authProvider.Authenticate(model.UserName, model.Password))
				{
					return Redirect(returnUrl ?? Url.Action("Index", "Admin"));
				}
				else
				{
					ModelState.AddModelError("", "Incorrect username or password");
					return View();
				}
			}
			else
			{
				return View();
			}
		}
	}
}

Создаем представление

Кликните правой кнопкой мыши по одному из методов действия в классе контроллера Account и выберите Add View из контекстного меню. Создайте строго типизированное представление под названием Login, которое использует LoginViewModel в качестве типа модели представления, как показано на рисунке 11-2. Отметьте флажком опцию Use a layout и выберите файл _AdminLayout.cshtml.

Рисунок 11-2: Добавляем представление Login

Нажмите кнопку Add, чтобы создать представление, и отредактируйте разметку в соответствии с листингом 11-8.

Листинг 11-8: Представление Login
@model SportsStore.WebUI.Models.LoginViewModel

@{
	ViewBag.Title = "Admin: Log In";
	Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<h1>Log In</h1>
<p>Please log in to access the administrative area:</p>

@using (Html.BeginForm())
{
	@Html.ValidationSummary(true)
	@Html.EditorForModel()
	<p><input type="submit" value="Log in" /></p>
}

Чтобы увидеть, как выглядит данное представление, запустите приложение и перейдите по ссылке /Admin/Index, как показано на рисунке 11-3.

Рисунок 11-3: Представление Login

Атрибут DataType указывает MVC Framework визуализировать редактор для свойства Password как HTML-элемент для ввода пароля, что означает, что символы пароля не видны. Атрибут Required, который мы применили к свойствам модели представления, включает валидацию на стороне клиента (мы уже добавили необходимые библиотеки JavaScript в файл _AdminLayout.cshtml в главе 10). Пользователи смогут отправить форму только тогда, когда предоставят и имя пользователя, и пароль. При вызове метода FormsAuthentication.Authenticate аутентификация будет проведена на сервере.

Внимание!

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

Когда мы получаем неправильные учетные данные, мы добавляем ошибку в ModelState и снова визуализируем представление. Это отображает наше сообщение в области информации о валидации, которую мы создали, вызвав в представлении вспомогательный метод Html.ValidationSummary.

Примечание

Обратите внимание, что в листинге 11-8 в нашем вызове вспомогательного метода Html.ValidationSummary мы выставили параметру bool значение true. Это исключает отображение сообщений о валидации свойств. Если бы мы этого не сделали, любые сообщения о валидации свойств дублировались бы в области информации о валидации и рядом с соответствующим элементом ввода.

Модульный тест: аутентификация

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

Мы можем выполнить эти тесты, создав имитированную реализацию интерфейса IAuthProvider и проверив тип и результат метода контроллера Login. Мы создали следующие тесты в новом файле тестов под названием AdminSecurityTests.cs:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SportsStore.WebUI.Controllers;
using SportsStore.WebUI.Infrastructure.Abstract;
using SportsStore.WebUI.Models;
using System.Web.Mvc;
namespace SportsStore.UnitTests
{
	[TestClass]
	public class AdminSecurityTests
	{
		[TestMethod]
		public void Can_Login_With_Valid_Credentials()
		{
			// Arrange - create a mock authentication provider
			Mock<IAuthProvider> mock = new Mock<IAuthProvider>();
			mock.Setup(m => m.Authenticate("admin", "secret")).Returns(true);
			// Arrange - create the view model

			LoginViewModel model = new LoginViewModel
			{
				UserName = "admin",
				Password = "secret"
			};
			// Arrange - create the controller
			AccountController target = new AccountController(mock.Object);
			// Act - authenticate using valid credentials
			ActionResult result = target.Login(model, "/MyURL");
			// Assert
			Assert.IsInstanceOfType(result, typeof(RedirectResult));
			Assert.AreEqual("/MyURL", ((RedirectResult)result).Url);
		}

		[TestMethod]
		public void Cannot_Login_With_Invalid_Credentials()
		{
			// Arrange - create a mock authentication provider
			Mock<IAuthProvider> mock = new Mock<IAuthProvider>();
			mock.Setup(m => m.Authenticate("badUser", "badPass")).Returns(false);
			// Arrange - create the view model
			LoginViewModel model = new LoginViewModel
			{
				UserName = "badUser",
				Password = "badPass"
			};
			// Arrange - create the controller
			AccountController target = new AccountController(mock.Object);
			// Act - authenticate using valid credentials
			ActionResult result = target.Login(model, "/MyURL");
			// Assert
			Assert.IsInstanceOfType(result, typeof(ViewResult));
			Assert.IsFalse(((ViewResult)result).ViewData.ModelState.IsValid);
		}
	}
}

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