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

ASP_NET_MVC_4_Framework_s_primerami_na_C_dlya_p

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

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

271

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

{

272

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

273

Нажмите кнопку 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). Пользователи смогут отправить форму только тогда, когда предоставят и имя пользователя, и

274

пароль. При вызове метода 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

275

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, который будет прикреплен к последующим запросам.

Загрузка изображений

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

Расширяем базу данных

Откройте окно Visual Studio Database Explorer и перейдите к таблице Products в базе, которую мы создали в главе 7. Название соединения для передачи данных может быть изменено на EFDbContext – это то название, которое мы назначили соединению в файле Web.config в главе 7. Visual Studio может быть немного непоследовательной, когда переименовывает соединения, так что возможно, что вы увидите оригинальное название, которое отображалось при создании соединения.

Щелкните правой кнопкой мыши в таблице и выберите Open Table Definition из контекстного меню. Добавьте определения для двух новых столбцов, которые показаны на рисунке 11-4.

276

Убедитесь, что вы правильно установили данные столбцов, и не забудьте отметить флажком опцию Allow Nulls для них обоих.

Рисунок 11-4: Добавляем новые столбцы в таблицу Products

Нажмите кнопку Update, после чего Visual Studio определит, какие операторы SQL нужно отправить в базу данных для ее обновления, и отобразит их в диалоговом окне Preview Database Updates, как показано на рисунке 11-5.

Рисунок 11-5: Предварительный просмотр обновлений базы данных

Нажмите кнопку Update Database для создания новых столбцов в базе данных.

277

Расширяем доменную модель

Нам нужно добавить два новых поля в класс Products проекта SportsStore.Domain, которые соответствуют столбцам, добавленным в базу данных. Дополнения выделены жирным шрифтом в листинге 11-9.

Листинг 11-9: Добавляем свойства в класс Products

using System.ComponentModel.DataAnnotations; using System.Web.Mvc;

namespace SportsStore.Domain.Entities

{

public class Product

{

[HiddenInput(DisplayValue = false)] public int ProductID { get; set; }

[Required(ErrorMessage = "Please enter a product name")] public string Name { get; set; }

[Required(ErrorMessage = "Please enter a description")] [DataType(DataType.MultilineText)]

public string Description { get; set; }

[Required]

[Range(0.01, double.MaxValue, ErrorMessage = "Please enter a positive price")] public decimal Price { get; set; }

[Required(ErrorMessage = "Please specify a category")] public string Category { get; set; }

public byte[] ImageData { get; set; }

[HiddenInput(DisplayValue = false)] public string ImageMimeType { get; set; }

}

}

Мы не хотим, чтобы какое-либо из этих новых свойств было видимым при визуализизации редактора. Для этого мы используем атрибут HiddenInput в свойстве ImageMimeType. Нам не нужно ничего делать со свойством ImageData, потому что платформа не визуализирует редактор для массивов байтов. Она делает это только для "простых" типов, таких как int, string, DateTime и так далее.

Внимание!

Убедитесь в том, что имена свойств, которые вы добавляете в класс Product, в

точности соответствуют названиям новых столбцов в базе данных.

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

Далее нам нужно добавить поддержку загрузки файлов. Для этого понадобиться создать пользовательский интерфейс, который будет использоваться администратором для загрузки изображений. Измените представление Views/Admin/Edit.cshtml в соответствии с листингом 11-10 (дополнения выделены жирным шрифтом).

Листинг 11-10: Добавляем поддержку изображений

@model SportsStore.Domain.Entities.Product

278

@{

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

}

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

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

FormMethod.Post, new { enctype = "multipart/form-data" })) { @Html.EditorForModel()

<div class="editor-label">Image</div> <div class="editor-field">

@if (Model.ImageData == null) { @:None

}

else {

<img width="150" height="150" src="@Url.Action("GetImage", "Product", new { Model.ProductID })" />

}

<div>Upload new image:

<input type="file" name="Image" /></div> </div>

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

}

Вы, возможно, уже знаете, что браузеры будут загружать файлы должным образом только тогда, когда атрибут enctype в HTML-элементе form содержит значение multipart/form-data. Другими словами, для успешной загрузки элемент form должен выглядеть следующим образом:

<form action="/Admin/Edit" enctype="multipart/form-data" method="post">

...

</form>

Без атрибута enctype браузер будет передавать только имя файла, а не его содержимое, что для нас совершенно бесполезно. Чтобы гарантировать наличие атрибута enctype, мы должны использовать перегруженный вспомогательный метод Html.BeginForm, который позволяет указать HTMLатрибуты, например:

@using (Html.BeginForm("Edit", "Admin", FormMethod.Post, new { enctype = "multipart/form-data" })) {

Обратите внимание, что если свойство ImageData отображаемого объекта Product не содержит null, мы добавляем элемент img и устанавливаем в качестве его источника результат вызова метода действия GetImage контроллера Product. Скоро мы это реализуем.

Сохраняем изображения в базе данных

Нам нужно расширить версию POST метода действия Edit класса AdminController так, чтобы мы могли принимать загруженное изображение и сохранять его в базе данных. Необходимые изменения показаны в листинге 11-11.

Листинг 11-11: Обработка изображения в классе AdminController

[HttpPost]

public ActionResult Edit(Product product, HttpPostedFileBase image)

{

if (ModelState.IsValid)

{

if (image != null)

279

{

product.ImageMimeType = image.ContentType; product.ImageData = new byte[image.ContentLength];

image.InputStream.Read(product.ImageData, 0, image.ContentLength);

}

repository.SaveProduct(product);

TempData["message"] = string.Format("{0} has been saved", product.Name); return RedirectToAction("Index");

}

else

{

// there is something wrong with the data values return View(product);

}

}

Мы добавили в метод Edit новый параметр, с помощью которого MVC Framework передает нам данные загруженного файла. Мы проверяем значение этого параметра, и если он не содержит null, то копируем данные и тип MIME из параметра в объект Product, чтобы сохранить их в базе данных.

Примечание

Понадобится обновить модульные тесты, чтобы отразить новый параметр в листинге 11-11. Передача параметра со значением null позволит тестам

скомпилироваться.

Мы также должны обновить класс EFProductRepository в проекте SportsStore.Domain и

гарантировать, что значения, присвоенные свойствам ImageData и ImageMimeType, сохраняются в базе данных. В листинге 11-12 показаны необходимые изменения в методе SaveProduct.

Листинг 11-12: Гарантируем, что данные изображений сохраняются в базе данных

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; dbEntry.ImageData = product.ImageData; dbEntry.ImageMimeType = product.ImageMimeType;

}

}

context.SaveChanges();

}

Реализуем метод действия GetImage

В листинге 11-10 мы добавили элемент img, содержание которого было получено с помощью метода действия GetImage. Теперь мы его реализуем, чтобы можно было отображать изображения,

280

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