[.NET] 一步步打造一个简单的 MVC 电商网站 - BooksStore(四)

一步步打造一个简单的 MVC 电商网站 - BooksStore(四)

  本系列的 GitHub地址:https://github.com/liqingwen2015/Wen.BooksStore

  《一步步打造一个简单的 MVC 电商网站 - BooksStore(一)》(发布时间:2017-03-30 )

      《一步步打造一个简单的 MVC 电商网站 - BooksStore(二)》(发布时间:2017-03-31)

  《一步步打造一个简单的 MVC 电商网站 - BooksStore(三)》(发布时间:2017-04-01)

  《一步步打造一个简单的 MVC 电商网站 - BooksStore(四)》(发布时间:2017-04-05)

简介

  上一节我们完成了两个主要功能:完成了整个购物车的流程,以及订单处理(发邮件进行通知),今天我们来学习一下最基本的增删改查,以及登录认证过滤器,加入防 CSRF 攻击,本系列已完结。

  该系列主要功能与知识点如下:

    分类、产品浏览、购物车、结算、CRUD(增删改查) 管理、发邮件、分页、模型绑定、认证过滤器和单元测试等。

     【备注】项目使用 VS2015 + C#6 进行开发,有问题请发表在留言区哦,还有,页面长得比较丑,请见谅。

 

目录

  • 基本的增删改查 CRUD
  • 登录授权认证过滤

 

基本的增删改查 CRUD

  我们创建一个新的控制器进行增删改查功能,AdminController,并添加一个显示所有数据的方法:

    /// <summary>
    /// 后台管理控制器
    /// </summary>
    public class AdminController : Controller
    {
        private readonly IBookRepository _bookRepository;

        public AdminController(IBookRepository bookRepository)
        {
            _bookRepository = bookRepository;
        }

        /// <summary>
        /// 首页
        /// </summary>
        /// <returns></returns>
        public ActionResult Index()
        {
            return View(_bookRepository.Books);
        }
    }

 

  不在沿用之前的布局页了,创建一个新的布局页 _AdmindLayout.cshtml:

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link href="~/Contents/admin/Site.css" rel="stylesheet" />
</head>
<body>
    <div>
        @RenderBody()
    </div>
</body>
</html>
.table {
    width: 100%;
    padding: 0;
    margin: 0;
}

    .table th {
        font: bold 12px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;
        color: #4f6b72;
        border-right: 1px solid #C1DAD7;
        border-bottom: 1px solid #C1DAD7;
        border-top: 1px solid #C1DAD7;
        letter-spacing: 2px;
        text-transform: uppercase;
        text-align: left;
        padding: 6px 6px 6px 12px;
        background: #CAE8EA no-repeat;
    }

    .table td {
        border-right: 1px solid #C1DAD7;
        border-bottom: 1px solid #C1DAD7;
        background: #fff;
        font-size: 14px;
        padding: 6px 6px 6px 12px;
        color: #4f6b72;
    }

        .table td.alt {
            background: #F5FAFA;
            color: #797268;
        }

    .table th.spec, td.spec {
        border-left: 1px solid #C1DAD7;
    }
Site.css

 

  对应的 Index.cshtml:

@model IEnumerable<Wen.BooksStore.Domain.Entities.Book>

@{
    Layout = "~/Views/Shared/_AdminLayout.cshtml";
}

<p>
    @Html.ActionLink("新增", "Edit")
</p>
<table class="table">
    <tr>
        <th>
            名称
        </th>
        <th>
            描述
        </th>
        <th>
            价格
        </th>
        <th>
            分类
        </th>
        <th></th>
    </tr>

    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Description)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Category)
            </td>
            <td>
                @Html.ActionLink("编辑", "Edit", new { id = item.Id })
                @using (Html.BeginForm("Delete", "Admin", FormMethod.Post, new { style = "display:inline;" }))
                {
                    @Html.Hidden("id", item.Id)
                    <input type="submit" value="删除" />
                }
            </td>
        </tr>
    }

</table>

 

 

 

   编辑,我把新增和编辑的位置放在一块,使用 id 进行区分,如果 id = 0 就表示新增的信息。

  在 AdminCtroller 中添加关于编辑的方法

        /// <summary>
        /// 编辑
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public ActionResult Edit(int id = 0)
        {
            if (id == 0)
            {
                return View(new Book());
            }

            var model = _bookRepository.Books.FirstOrDefault(x => x.Id == id);
            return View(model);
        }

        /// <summary>
        /// 编辑
        /// </summary>
        /// <param name="book"></param>
        /// <returns></returns>
        [HttpPost]
        public ActionResult Edit(Book book)
        {
            if (!ModelState.IsValid)
            {
                return View(book);
            }

            _bookRepository.SaveBook(book);
            return RedirectToAction("Index");
        }

 

  更新存储库中的方法:

  IBookRepository.cs

    /// <summary>
    /// 书存储库接口
    /// </summary>
    public interface IBookRepository
    {
        /// <summary>
        /// 书模型集合
        /// </summary>
        IQueryable<Book> Books { get; }

        /// <summary>
        /// 保存书
        /// </summary>
        /// <param name="book"></param>
        /// <returns></returns>
        int SaveBook(Book book);

        /// <summary>
        /// 删除书
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        Book DeleteBook(int id);
    }

 

  EfBookRepository.cs

    /// <summary>
    /// 书存储库
    /// </summary>
    public class EfBookRepository : IBookRepository
    {
        private readonly EfDbContext _context = new EfDbContext();

        /// <summary>
        /// 书模型集合
        /// </summary>
        public IQueryable<Book> Books => _context.Books;

        /// <summary>
        /// 保存书
        /// </summary>
        /// <param name="book"></param>
        /// <returns></returns>
        public int SaveBook(Book book)
        {
            if (book.Id == 0)
            {
                _context.Books.Add(book);
            }
            else
            {
                var model = _context.Books.Find(book.Id);

                if (model==null)
                {
                    return 0;
                }

                model.Category = book.Category;
                model.Description = book.Description;
                model.Name = book.Name;
                model.Price = book.Price;
            }

            return _context.SaveChanges();
        }

        /// <summary>
        /// 删除书
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public Book DeleteBook(int id)
        {
            var model = _context.Books.Find(id);

            if (model == null)
            {
                return null;
            }

            _context.Books.Remove(model);
            _context.SaveChanges();

            return model;
        }
    }

 

  需要对 Book 模型加上验证用的特性:

    [Table("Book")]
    public class Book
    {
        /// <summary>
        /// 标识
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// 名称
        /// </summary>
        [Required(ErrorMessage = "名称不能为空")]
        public string Name { get; set; }

        /// <summary>
        /// 描述
        /// </summary>
        [Required(ErrorMessage = "描述不能为空")]
        public string Description { get; set; }

        /// <summary>
        /// 价格
        /// </summary>
        [Required(ErrorMessage = "价格不能为空")]
        [Range(0.01, double.MaxValue, ErrorMessage = "请填写合适的价格")]
        public decimal Price { get; set; }

        /// <summary>
        /// 分类
        /// </summary>
        [Required(ErrorMessage = "分类不能为空")]
        public string Category { get; set; }
    }

 

  _AdminLayout.cshtml 需要引入验证用的 js(客户端验证):

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

 

  Edit.cshtml

@model Wen.BooksStore.Domain.Entities.Book

@{
    Layout = "~/Views/Shared/_AdminLayout.cshtml";
}

<h2>编辑</h2>

<div>
    @Html.ValidationSummary()

    <div>
        @using (Html.BeginForm())
        {
            @Html.HiddenFor(x => x.Id)
            <table>
                <tr>
                    <td>名称</td>
                    <td>@Html.TextBoxFor(x => x.Name)</td>
                </tr>
                <tr>
                    <td>价格</td>
                    <td>@Html.TextBoxFor(x => x.Price)</td>
                </tr>
                <tr>
                    <td>分类</td>
                    <td>@Html.TextBoxFor(x => x.Category)</td>
                </tr>
                <tr>
                    <td>描述</td>
                    <td>@Html.TextAreaFor(x => x.Description)</td>
                </tr>
            </table>
            <input type="submit" value="提交" />
        }
    </div>
</div>

图:错误提示

 

  删除

        /// <summary>
        /// 删除
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpPost]
        public ActionResult Delete(int id)
        {
            _bookRepository.DeleteBook(id);
            return RedirectToAction("Index");
        }

 

  加入提示,我们在新增、编辑和删除时应该加入必要的提示信息,使用 TempData。

 

  /Admin/Index.cshtml 下的也要添加:

 

  执行效果:

 

  【备注】TempData 临时数据保存了一条信息,是一个“键/值”字典,类似会话 Session 和 ViewBag,它和 Session 的差别是,在 HTTP 请求结束后会被删除。因为这里使用了 RedirectToAction ,一条重定向指令,会告诉浏览器重定向请求到一个新地址,这时就不能使用 ViewBag,ViewBag 用于在控制器与视图之间传递数据,但它保持数据的时间不能比当前的 HTTP 请求长,重定向意味着用户是跨请求的,ViewBag 不能用于跨请求时传递数据。

 

登录授权认证过滤

  上面是一个 Admin 的后台管理操作,不是每一个用户都能够进入管理的,所以现在加入登录授权认证功能,只有成功后,才能进入管理界面。

  先在配置文件 WebConfig.cs 中加入

<authentication mode="Forms">
      <forms loginUrl="~/Account/Login" timeout="2880">
        <credentials passwordFormat="Clear">
          <user name="admin" password="123"/>
        </credentials>
      </forms>
</authentication>
<?xml version="1.0" encoding="utf-8"?>
<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=301880
  -->
<configuration>
  <connectionStrings>
    <add name="EfDbContext" connectionString="server=.;database=TestDb;uid=sa;pwd=123" providerName="System.Data.SqlClient"/>
  </connectionStrings>

  <appSettings>
    <add key="webpages:Version" value="3.0.0.0"/>
    <add key="webpages:Enabled" value="false"/>
    <add key="ClientValidationEnabled" value="true"/>
    <add key="UnobtrusiveJavaScriptEnabled" value="true"/>
    <add key="SendEmailName" value="943239005@qq.com"/>
  </appSettings>
  <system.web>
    <authentication mode="Forms">
      <forms loginUrl="~/Account/Login" timeout="2880">
        <credentials passwordFormat="Clear">
          <user name="admin" password="123"/>
        </credentials>
      </forms>
    </authentication>
    <compilation debug="true" targetFramework="4.6.1"/>
    <httpRuntime targetFramework="4.6.1"/>
    <httpModules>
      <add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web"/>
    </httpModules>
  </system.web>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35"/>
        <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0"/>
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35"/>
        <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0"/>
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35"/>
        <bindingRedirect oldVersion="1.0.0.0-5.2.3.0" newVersion="5.2.3.0"/>
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
  <system.codedom>
    <compilers>
      <compiler language="c#;cs;csharp" extension=".cs"
        type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
        warningLevel="4" compilerOptions="/langversion:6 /nowarn:1659;1699;1701"/>
      <compiler language="vb;vbs;visualbasic;vbscript" extension=".vb"
        type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
        warningLevel="4" compilerOptions="/langversion:14 /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+"/>
    </compilers>
  </system.codedom>
  <system.webServer>

    <validation validateIntegratedModeConfiguration="false"/>
    <modules>
      <remove name="ApplicationInsightsWebTracking"/>
      <add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web"
        preCondition="managedHandler"/>
    </modules>
  </system.webServer>
</configuration>
WebConfig.cs

  

  在这里使用的授权认证模式为表单认证,为了简化与数据库的交互操作,采取的是硬编码的形式。如果尚未得到认证,会跳转到 Account/Login 的地址让管理员先进行登录,timeout 表示登录(即认证)成功的保持时长为 2880 分钟(即 48 小时),而 name 表示的就是用户名, password 表示的就是登录密码。  

 

  这里采用的是授权认证过滤器,我们需要对要认证后才能进入的控制器添加一个特性[Authorize],即对 AdminController 添加该特性。

 

 

  新建表单认证提供器,一个接口和一个实现:

  IAuthProvider.cs:

    public interface IAuthProvider
    {
        /// <summary>
        /// 认证
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        bool Auth(string userName, string password);
    }

 

  FormsAuthProvider.cs:

    /// <summary>
    /// 表单认证提供者
    /// </summary>
    public class FormsAuthProvider:IAuthProvider
    {
        /// <summary>
        /// 认证
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        public bool Auth(string userName, string password)
        {
            var result = FormsAuthentication.Authenticate(userName, password);

            if (result)
            {
                //设置认证 Cookie
                FormsAuthentication.SetAuthCookie(userName, false);
            }

            return result;
        }
    }

  

  AddBindings() 方法中注册:

        /// <summary>
        /// 添加绑定
        /// </summary>
        private void AddBindings()
        {
            _kernel.Bind<IBookRepository>().To<EfBookRepository>();
            _kernel.Bind<IOrderProcessor>().To<EmailOrderProcessor>();
            _kernel.Bind<IAuthProvider>().To<FormsAuthProvider>();
        }

 

    /// <summary>
    /// 登录视图模型
    /// </summary>
    public class LoginViewModel
    {
        [Required(ErrorMessage = "用户名不能为空")]
        public string UserName { get; set; }

        [Required(ErrorMessage = "密码不能为空")]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }

 

  新建 AccountController

 

    public class AccountController : Controller
    {
        private readonly IAuthProvider _authProvider;

        public AccountController(IAuthProvider authProvider)
        {
            _authProvider = authProvider;
        }

        /// <summary>
        /// 登录
        /// </summary>
        /// <returns></returns>
        public ActionResult Login()
        {
            return View();
        }

        /// <summary>
        /// 登录
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Login(LoginViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(new LoginViewModel());
            }

            var result = _authProvider.Auth(model.UserName, model.Password);
            if (result) return RedirectToAction("Index", "Admin");

            ModelState.AddModelError("", "账号或用户名有误");
            return View(new LoginViewModel());
        }
    }

 

  Login.cshtml 登录页面:

@model Wen.BooksStore.WebUI.Models.LoginViewModel
@{
    Layout = null;
}

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>登录</title>
    @*<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">*@
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
    @*<link href="~/Contents/Login/css/htmleaf-demo.css" rel="stylesheet" />*@
    <style type="text/css">
        @@import url(https://fonts.googleapis.com/css?family=Roboto:300);

        .login-page {
            margin: auto;
            padding: 8% 0 0;
            width: 360px;
        }

        .form {
            background: #FFFFFF;
            box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
            margin: 0 auto 100px;
            max-width: 360px;
            padding: 45px;
            position: relative;
            text-align: center;
            z-index: 1;
        }

        .form input {
            background: #f2f2f2;
            border: 0;
            box-sizing: border-box;
            font-family: "Roboto", sans-serif;
            font-size: 14px;
            margin: 0 0 15px;
            outline: 0;
            padding: 15px;
            width: 100%;
        }

        .form button {
            -webkit-transition: all 0.3 ease;
            background: #4CAF50;
            border: 0;
            color: #FFFFFF;
            cursor: pointer;
            font-family: "Microsoft YaHei", "Roboto", sans-serif;
            font-size: 14px;
            outline: 0;
            padding: 15px;
            text-transform: uppercase;
            transition: all 0.3 ease;
            width: 100%;
        }

        .form button:hover, .form button:active, .form button:focus { background: #43A047; }

        .form .message {
            color: #b3b3b3;
            font-size: 12px;
            margin: 15px 0 0;
        }

        .form .message a {
            color: #4CAF50;
            text-decoration: none;
        }

        .form .register-form { display: none; }

        .container {
            margin: 0 auto;
            max-width: 300px;
            position: relative;
            z-index: 1;
        }

        .container:before, .container:after {
            clear: both;
            content: "";
            display: block;
        }

        .container .info {
            margin: 50px auto;
            text-align: center;
        }

        .container .info h1 {
            color: #1a1a1a;
            font-size: 36px;
            font-weight: 300;
            margin: 0 0 15px;
            padding: 0;
        }

        .container .info span {
            color: #4d4d4d;
            font-size: 12px;
        }

        .container .info span a {
            color: #000000;
            text-decoration: none;
        }

        .container .info span .fa { color: #EF3B3A; }

        body {
            -moz-osx-font-smoothing: grayscale;
            -webkit-font-smoothing: antialiased;
            background: #76b852; /* fallback for old browsers */
            background: -webkit-linear-gradient(right, #76b852, #8DC26F);
            background: -moz-linear-gradient(right, #76b852, #8DC26F);
            background: -o-linear-gradient(right, #76b852, #8DC26F);
            background: linear-gradient(to left, #76b852, #8DC26F);
            font-family: "Roboto", sans-serif;
        }
    </style>
    <!--[if IE]>
        <script src="http://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
    <![endif]-->
    <script src="~/Scripts/jquery-1.10.2.js"></script>
    <script src="~/Scripts/jquery.validate.js"></script>
    <script src="~/Scripts/jquery.validate.unobtrusive.js"></script>
</head>
<body>
    <div id="wrapper" class="login-page">
        <div id="login_form" class="form">
            @using (Html.BeginForm("Login", "Account", FormMethod.Post, new { @class = "login-form" }))
            {
                <span style="float: left; color: red;">@Html.ValidationSummary()</span>
                @Html.AntiForgeryToken()
                @Html.TextBoxFor(x => x.UserName, new { placeholder = "用户名" })
                @Html.EditorFor(x => x.Password, new { placeholder = "密码",  })

                <input type="submit" value="登 录" />
            }

        </div>
    </div>

</body>
</html>
Login.cshtml

 

 

  【备注】ValidateAntiForgeryToken 特性用于防止跨站请求伪造(CSRF)攻击。

 


【博主】反骨仔

【原文】http://www.cnblogs.com/liqingwen/p/6658975.html 

【改编与参考】《精通 ASP.NET MVC 5》

 

posted @ 2017-04-05 09:07  反骨仔  阅读(5720)  评论(7编辑  收藏  举报