13.SportsStore之安全性与收尾工作

  • 时间:2019-06-19
  • 作者:Charles
  • 热度:3397

在前文中添加了对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>

当客户浏览产品列表时,他们将会看到产品的图像,如下所示。



博主声明

1、本博客主要为原创文章,转载请注明出处。

2、部分文章来自网络,已注明出处,如有侵权请与本人联系。

3、如果文章内容有误,或者您有其他更好的意见、建议请给我留言,我会及时处理!