Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

ASP_NET_MVC_4_Framework_s_primerami_na_C_dlya_p

.pdf
Скачиваний:
25
Добавлен:
19.03.2016
Размер:
17.66 Mб
Скачать

Рисунок 10-13: Валидация данных при редактировании товаров

Включаем валидацию на стороне клиента

На данный момент валидация данных применяется только тогда, когда администратор отправляет поправки на сервер. Но, если есть проблемы с введенными данными, необходимо предоставить пользователям немедленную обратную связь. Поэтому разработчикам часто требуется выполнять валидацию на стороне клиента, то есть проверку данных браузером с помощью JavaScript. MVC Framework может выполнять валидацию на стороне клиента на основе атрибутов DataAnnotations, которые мы применили к классу доменной модели.

Эта функция включена по умолчанию, но до сих пор она не работала, потому что мы не добавили ссылки на необходимые библиотеки JavaScript. Проще всего добавить эти ссылки в файл _AdminLayout.cshtml, чтобы валидация на стороне клиента могла работать на любой странице, которая использует этот макет. В листинге 10-15 показаны изменения в макете. Функция валидации на стороне клиента MVC основана на JavaScript-библиотеке JQuery, что понятно из имен файлов сценария.

261

Листинг 10-15: Импорт файлов JavaScript для валидации на стороне клиента

<!DOCTYPE html>

<html>

<head>

<meta name="viewport" content="width=device-width" />

<link href="~/Content/Admin.css" rel="stylesheet" type="text/css" />

<script src="~/Scripts/jquery-1.7.1.js"></script> <script src="~/Scripts/jquery.validate.min.js"></script>

<script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>

<title></title>

</head>

<body>

<div>

@if (TempData["message"] != null) {

<div class="Message">@TempData["message"]</div>

}

@RenderBody()

</div>

</body>

</html>

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

HtmlHelper.ClientValidationEnabled = false;

HtmlHelper.UnobtrusiveJavaScriptEnabled = false;

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

<configuration>

<appSettings>

<add key="ClientValidationEnabled" value="false"/> <add key="UnobtrusiveJavaScriptEnabled" value="false"/>

</appSettings>

</configuration>

Создаем новые товары

Далее мы реализуем метод действия Create, на который ведет ссылка Add a new product на странице со списком товаров. Он позволит администратору добавлять новые элементы в каталог товаров. Чтобы добавить поддержку создания новых товаров, потребуется только одно небольшое дополнение и одно изменение в нашем приложении. Это отличный пример возможностей и гибкости хорошо продуманного приложения MVC. Во-первых, добавьте метод Create, показанный в листинге

10-16, в класс AdminController.

Листинг 10-16: Добавляем метод действия Create в AdminController

public ViewResult Create() {

return View("Edit", new Product());

}

262

Метод Create не визуализирует свое представление по умолчанию. Вместо этого он указывает, что должно быть использовано представление Edit. То, что один метод действия использует представление, обычно связанное с другим представлением, является вполне приемлемым. В данном случае мы внедряем новый объект Product в качестве модели представления, так что представление Edit заполняется пустыми полями.

Это приводит нас к следующей модификации. Обычно мы ожидаем, что форма отправляет запрос к действию, которое ее визуализировало, и именно это по умолчанию предполагает Html.BeginForm, когда генерирует HTML-форму. Тем не менее, для нашего метода Create это не работает, потому что мы хотим, чтобы форма отправляла запрос к действию Edit, чтобы можно было сохранить добавленные данные о товаре. Чтобы это исправить, можно использовать перегруженную версию вспомогательного метода Html.BeginForm, чтобы указать, что целью формы, сгенерированной в представлении Edit, является метод действия Edit контроллера Admin. Листинг 10-17 содержит изменение, которое мы внесли в файл представления Views/Admin/Edit.cshtml.

Листинг 10-17: Явно указываем метод действия и контроллер для формы

@model SportsStore.Domain.Entities.Product @{

ViewBag.Title = "Admin: Edit " + @Model.Name; Layout = "~/Views/Shared/_AdminLayout.cshtml";

}

<h1>Edit @Model.Name</h1>

@using (Html.BeginForm("Edit", "Admin"))

{

@Html.EditorForModel()

<input type="submit" value="Save" /> @Html.ActionLink("Cancel and return to List", "Index")

}

Теперь форма всегда будет отправлена действию Edit независимо от того, каким действием она была визуализирована. Мы можем создавать товары, перейдя по ссылке Add a new product и заполнив поля, как показано на рисунке 10-14.

Рисунок 10-14 : Добавляем новый товар в каталог

263

Удаляем товары

Добавить поддержку для удаления товаров довольно просто. Для начала мы добавим новый метод в интерфейс IProductRepository, как показано в листинге 10-18.

Листинг 10-18: Добавляем метод для удаления товаров

using System.Linq;

using SportsStore.Domain.Entities; namespace SportsStore.Domain.Abstract

{

public interface IProductRepository

{

IQueryable<Product> Products { get; } void SaveProduct(Product product);

Product DeleteProduct(int productID);

}

}

Далее мы реализуем этот метод в классе хранилища Entity Framework, EFProductRepository, как показано в листинге 10-19.

Листинг 10-19: Реализуем поддержку удаления в классе хранилища Entity Framework

using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System.Linq;

namespace SportsStore.Domain.Concrete

{

public class EFProductRepository : IProductRepository

{

private EFDbContext context = new EFDbContext(); public IQueryable<Product> Products

{

get { return context.Products; }

}

public void SaveProduct(Product product)

{

if (product.ProductID == 0)

{

context.Products.Add(product);

}

else

{

Product dbEntry = context.Products.Find(product.ProductID); if (dbEntry != null)

{

dbEntry.Name = product.Name; dbEntry.Description = product.Description; dbEntry.Price = product.Price; dbEntry.Category = product.Category;

}

}

context.SaveChanges();

}

public Product DeleteProduct(int productID)

{

Product dbEntry = context.Products.Find(productID); if (dbEntry != null)

{

context.Products.Remove(dbEntry);

context.SaveChanges();

}

return dbEntry;

264

}

}

}

Последним шагом будет реализация метода действия Delete в контроллере Admin. Этот метод действия будет поддерживать только запросы POST, так как удаление объектов не является идемпотентной операцией. Как мы объясним в главе 14, браузеры и кэши могут делать запросы GET без явного согласия пользователя, поэтому мы должны проявить осторожность, чтобы избежать внесения изменений в результате запросов GET. Листинг 10-20 содержит новый метод действия.

Листинг 10-20: Метод действия Delete

[HttpPost]

public ActionResult Delete(int productId) {

Product deletedProduct = repository.DeleteProduct(productId); if (deletedProduct != null) {

TempData["message"] = string.Format("{0} was deleted", deletedProduct.Name);

}

return RedirectToAction("Index");

}

Модульный тест: удаление товаров

Мы хотим протестировать базовое поведение метода действия Delete: когда действительный ProductID передается ему в качестве параметра, он должен вызвать метод хранилища DeleteProduct и передать значение ProductID, которое должно быть удалено. Вот тест:

[TestMethod]

public void Can_Delete_Valid_Products()

{

// Arrange - create a Product

Product prod = new Product { ProductID = 2, Name = "Test" };

//Arrange - create the mock repository Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products).Returns(new Product[] {

new Product {ProductID = 1, Name = "P1"}, prod,

new Product {ProductID = 3, Name = "P3"}, }.AsQueryable());

//Arrange - create the controller

AdminController target = new AdminController(mock.Object);

//Act - delete the product target.Delete(prod.ProductID);

//Assert - ensure that the repository delete method was

//called with the correct Product

mock.Verify(m => m.DeleteProduct(prod.ProductID));

}

Вы сможете увидеть работу новой функции, просто нажав одну из кнопок Delete на странице со списком товаров, как показано на рисунке 10-15. Как видите, мы воспользовались переменной TempData для отображения сообщения о том, что продукт будет удален из каталога.

265

Рисунок 10-15: Удаляем товар из каталога

Резюме

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

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

266

SportsStore: безопасность и последние штрихи

В предыдущей главе мы добавили поддержку администрирования приложения SportsStore, и от вашего внимания не должен был ускользнуть тот факт, что любой пользователь сможет изменить каталог товаров, если мы развернем приложение прямо сейчас. Для этого нужно только знать, что функции администрирования доступны по ссылке Admin/Index. В этой главе мы покажем вам, как предотвратить использование средств администрирования случайными пользователями, защитив доступ к контроллеру Admin с помощью пароля.

Когда безопасность будет реализована, мы завершим приложение SportsStore, добавив поддержку изображения товаров. Это может показаться простой функцией, но она требует применения некоторых интересных техник MVC.

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

Поскольку 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 будет достаточно более простой аутентификации.

267

Для начала мы создадим имя пользователя и пароль, которые будут предоставлять доступ к функциям администрирования 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...

}

}

268

Атрибут 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 работает.

269

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

Для использования 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;

}

}

}

270

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]