zoukankan      html  css  js  c++  java
  • [.NET] 一步步打造一个简单的 MVC 电商网站

    一步步打造一个简单的 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;
                 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- 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;
                 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;
                 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- 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》

  • 相关阅读:
    2019 | 开启新的堕落生活
    2018博客之星评选,我非常需要您宝贵的一票!♪(・ω・)ノ
    前端开发 2018 回顾
    全栈设计模式套餐MVVM, RESTful, MVC的历史探索
    停止学习框架
    那些被浏览器阻止的模拟事件...
    Just Cause系列游戏品鉴
    GPU硬件加速原理 /转
    快速上手最棒的网格框架ag-Grid
    用户数据验证的正确姿势之assert
  • 原文地址:https://www.cnblogs.com/liqingwen/p/6658975.html
Copyright © 2011-2022 走看看