在前文中添加了对SportsStore应用程序进行管理的支持。如果现在部署这个应用程序,任何人都可以修改产品,只要知道Admin/Index地址,这是极不安全的。本文将演示如何密码保护对Admin控制器的访问,来防止任意的人使用管理功能。
有了适当的安全机制后,还将实现一个添加产品图片的功能,以进一步完善SportsStore应用程序。
一:实现管理控制器的安全
由于ASP.NET MVC建立在核心的ASP.NET平台之上,使用户已经拥有了对ASP.NET FormsAuthentication(ASP.NET表单认证)工具的访问,该工具是对已登陆人员保持跟踪的一个通用系统。但对于本示例应用程序,本文只简单地演示如何建立最简单的配置。
<system.web> <authentication mode="Forms"> <forms loginUrl="~/Account/Login" timeout="2880"/> </authentication> </system.web>
在一个用Basic模板创建的MVC应用程序中,表单认证是自动启用的。loginUrl属性告诉ASP.NET,当用户需要对自己进行认证时,应该将他们定向到哪一个URL,这里是~/Account/Login页面。Timeout属性指明了被认证用户登陆后额保持时间,单位是分钟,默认时间是48小时。
首先要创建一个允许对SportsStore管理特性进行访问的用户名和口令。修改Web.config文件的authentication节点如下。
<authentication mode="Forms"> <forms loginUrl="~/Account/Login" timeout="2880"> <credentials passwordFormat="Clear"> <user name="admin" password="123456" /> </credentials> </forms> </authentication>
简单起见,在Web.config文件中硬编码了一个用户名和口令。
1.1运用过滤器进行授权
MVC框架有一个叫过滤器(Filter)的功能强大的特性。这些过滤器是一些.NET注解属性,可以把它们运用于动作方法或控制器。当一个请求被处理时,它们可以引入一些附加逻辑。过滤器有许多不同的种类,而且也可以创建自己的自定义过滤器。本例将使用的过滤器是默认的授权过滤器Authorize,将把它运用于AdminController类,如下所示。
using SportsStore.Domain.Abstrace; using SportsStore.Domain.Entities; 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; } // 省略其他代码 } }
当不带任何参数地运用时,如果用户已被认证,这个Authorize注解属性便允许访问该控制器的动作方法。
运行应用程序,并导航到/Admin/Index网址,便可以看到Authorize过滤器所具有的效果。将会看到如下所示的错误页面。
当用户试图访问Admin控制器的Index动作方法时,MVC检测到了Authorize过滤器。由于用户还没有被认证,因此被重定向到Web.config表单认证小节所指定的loginUrl:~/Account/Login。由于还没有创建Account控制器,所以将会报错,此时已经证明Authorize过滤器正在起作用。
1.2创建认证提供器
接下来的任务是创建Web.config文件中所引用的Account控制器和Login动作方法。首先要创建一个将在控制器和视图之间传递的视图模型类。在SportsStore.WebUI项目的Models文件夹中添加一个名称为LoginViewModel的新类,编辑其内容如下所示。
using System.ComponentModel.DataAnnotations; namespace SportsStore.WebUI.Models { public class LoginViewModel { [Required(ErrorMessage ="请输入{0}")] [Display(Name ="用户名")] public string UserName { get; set; } [Required(ErrorMessage = "请输入{0}")] [Display(Name = "密码")] [DataType(DataType.Password)] public string Password { get; set; } } }
接下来创建一个名为AccountController的新控制器,如下所示。
using SportsStore.WebUI.Infrastructure.Abstract; using SportsStore.WebUI.Models; using System.Web.Mvc; namespace SportsStore.WebUI.Controllers { public class AccountController : Controller { IAuthProvider authProvider; public AccountController(IAuthProvider auth) { authProvider = auth; } public ActionResult Login() { authProvider.Logout(); 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("", "用户名或密码不正确"); return View(); } } else return View(); } } }
1.4创建视图
右击Account控制器中的Login动作方法,从菜单中选择添加视图,创建一个名为Login的强类型视图,选择LoginViewModel为视图模型类,布局页选择_AdminLayout.cshtml文件。编辑视图文件如下所示。
@model SportsStore.WebUI.Models.LoginViewModel @{ ViewBag.Title = "管理员登录"; Layout = "~/Views/Shared/_AdminLayout.cshtml"; } <h2>管理员登录</h2> <hr /> <div> @using (Html.BeginForm()) { @Html.ValidationSummary(true) @Html.EditorForModel() <p><input type="submit" value="登录" class="btn btn-primary" /></p> } </div>
运行此应用程序并导航到/Admin/Index,便可以看到该视图的外观,如下所示。
DataType注解属性让MVC框架把Password属性的编辑器渲染成一个HTML的password-input元素,这意味着密码字段中的字符是不可见的。
通过以上工作,起到了保护SportsStore管理功能的作用。仅当用户提供了合法凭据并接收了一个cookie之后,才允许访问这些特性。客户端所接收的cookie将被附加到后继的请求中。
二:图像上传
本节将为管理员添加上传产品图像并存储到数据库中的能力,以使这些图像能够在产品分类中被显示出来。
2.1扩展数据库
打开数据库管理器,右击Product表,在菜单中选择设计,新增两个新列并保存,如下所示。
2.2增强域模型
需要对SportsStore.Domain项目的Product类添加两个新字段,它们对应于刚才添加到数据库的列。如下所示。
using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Web.Mvc; namespace SportsStore.Domain.Entities { [Serializable()] public class Product { [HiddenInput(DisplayValue =false)] public int ProductID { get; set; } [DisplayName("名称")] [Required(ErrorMessage ="请输入{0}")] public string Name { get; set; } [DataType(DataType.MultilineText)] [DisplayName("简介")] [Required(ErrorMessage = "请输入{0}")] public string Description { get; set; } [DisplayName("价格")] [Required(ErrorMessage = "请输入{0}")] [Range(0.01, double.MaxValue, ErrorMessage = "{0}必须在{1}和{2}之间")] public decimal Price { get; set; } [DisplayName("类别")] [Required(ErrorMessage = "请输入{0}")] public string Category { get; set; } public byte[] ImageData { get; set; } [HiddenInput(DisplayValue =false)] public string ImageMimeType { get; set; } } }
在MVC框架渲染编辑器时,用户不希望这两个新属性时可见的,于是在ImageMimeType属性上使用HiddenInput注解属性。不需要对ImageData属性做任何设置,因为框架不会为一个字节数组渲染一个编辑器。
2.3创建上传用户界面的元素
下一步是添加进行文件上传的支持,这包括创建一个管理员可以用来上传图像的UI。修改Views/Admin/Edit.cshtml视图,如下所示。
@model SportsStore.Domain.Entities.Product @{ ViewBag.Title = Model.ProductID > 0 ? "产品编辑" : "产品添加"; Layout = "~/Views/Shared/_AdminLayout.cshtml"; } <h2>@(Model.ProductID > 0 ? "产品编辑" : "产品添加")</h2> <hr /> <div> @using (Html.BeginForm("Edit","Admin",FormMethod.Post,new {enctype="multipart/form-data" })) { @Html.EditorForModel() <div><label for="Image">图片</label></div> <div> @if (Model.ImageData==null) { @:无 } else { <img width="150" height="150" src="@Url.Action("GetImage","Product",new {Model.ProductID})" /> } <p>上传图片:<input type="file" name="Image" /></p> </div> <input type="submit" value="保存" class="btn btn-primary" /> @Html.ActionLink("取消并返回列表", "Index") } </div>
只有当HTML的form元素定义了一个值为multipart/form-data的enctype时,Web浏览器才会适当地上传文件。换句话说,要成功上传,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" }))
2.4将图像保存到数据库
需要增强AdminController中POST版本的Edit动作方法,以取得上传的图像数据,并把它保存到数据库中。如下所示。
[HttpPost] public ActionResult Edit(Product product,HttpPostedFileBase image) { if (ModelState.IsValid) { if (image!=null) { product.ImageMimeType = image.ContentType; product.ImageData = new byte[image.ContentLength]; image.InputStream.Read(product.ImageData, 0, image.ContentLength); } repository.SaveProduct(product); TempData["msg"] = string.Format("{0}已成功保存", product.Name); return RedirectToAction("Index"); } else return View(product); }
对Edit方法添加了一个新参数,MVC框架把它用于传递上传文件的数据。检查该参数的值是否为空,若非空,便把这些数据和该参数的MIME类型拷贝到Product对象,以便把它们保存到数据库。
还要更新SportsStore.Domain项目中的EFProductRepository类,以确保分配给ImageData和ImageMimeType属性的值被保存到数据库,修改SaveProduct方法如下所示。
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(); }
2.5实现GetImage动作方法
添加一个GetImage动作方法到ProductController类中,以便能够显示数据库中的图像,如下所示。
public FileContentResult GetImage(int productId) { Product pro = repository.Products.FirstOrDefault(p => p.ProductID == productId); if (pro != null) return File(pro.ImageData, pro.ImageMimeType); else return null; }
此方法试图找到一个与参数指定的ID相匹配的产品。当希望将一个文件返回给客户端浏览器时,动作方法应当返回FileContentResult类,其实例是用控制器基类的File方法创建的。
管理员现在可以上传产品的图像了,启动应用程序,导航到Admin/Index网址,编辑一个产品,点击选择文件,并上传保存,将会看到如下页面。
2.6显示产品图像
剩下的工作是在产品列表页显示这个图像,编辑Views/Shared/ProductSummary.cshtml视图,如下所示。
@model SportsStore.Domain.Entities.Product <div class="col-md-3 col-sm-6 col-xs-6"> @if (Model.ImageData!=null) { <p><img style="height:100%; max-height:160px;max-width:100%" src="@Url.Action("GetImage","Product",new {Model.ProductID})" /></p> } <h3>@Model.Name</h3> @Model.Description @using (Html.BeginForm("AddToCart","Cart")) { @Html.HiddenFor(x=>x.ProductID) @Html.Hidden("returnUrl",Request.Url.PathAndQuery) <input type="submit" value="+ 加到购物车" class="btn btn-warning btn-xs" /> } <h4>@Model.Price.ToString("c")</h4> </div>
当客户浏览产品列表时,他们将会看到产品的图像,如下所示。