zoukankan      html  css  js  c++  java
  • 跟我学ASP.NET MVC之十:SportsStrore安全

     摘要:

    在之前的文章中,我给SportsStore应用程序添加了产品管理功能,这样一旦我发布了网站,任何人都可能修改产品信息,而这是你必须考虑的。他们只需要知道你的网站有这个功能,以及功能的访问路径是/Admin/Index。我将向你介绍如何通过对Admin控制器实现密码保护来防止任意的人员使用管理功能。

    创建基本安全策略

    我将从配置表单身份验证开始,它是用户在ASP.NET应用程序身份验证的一种方式。修改Web.config文件的System.Web节,添加authentication子节点。

    1   <system.web>
    2     <compilation debug="true" targetFramework="4.5.1" />
    3     <httpRuntime targetFramework="4.5.1" />
    4     <authentication mode="Forms">
    5       <forms loginUrl="~/Account/Login" timeout="2880" >
    6       </forms>
    7     </authentication>
    8   </system.web>

    该配置表示,使用表单验证。如果表单验证失败,则网页定向到/Account/Login页面,验证有效期时间是2880分钟(48小时)。

    还有其他的身份验证方式,另一个常用的是Windows方式验证。它使用User Group或者Active Directory验证。这里不打算介绍它。读者可以在网上搜索这方面的知识。

    还可以给该表单验证配置添加credential。

     1   <system.web>
     2     <compilation debug="true" targetFramework="4.5.1" />
     3     <httpRuntime targetFramework="4.5.1" />
     4     <authentication mode="Forms">
     5       <forms loginUrl="~/Account/Login" timeout="2880" >
     6         <credentials passwordFormat="Clear">
     7           <user name="user" password="123"/>
     8         </credentials>
     9       </forms>
    10     </authentication>
    11   </system.web>

    该credentials配置表示,在配置文件中使用密码明文(不推荐),登录用户名是user,密码是123。

    使用过滤器应用表单验证

    MVC框架有一个强大的名叫过滤器的功能。他们是你可以运用在Action方法或者控制器类上的.NET特性,当一个请求发送过来将改变MVC框架行为的时候,引入额外的业务逻辑。

    这里我将把它修饰AdminController控制器类,它将给该控制器内的所有Action方法添加这个过滤器。

    1     [Authorize]
    2     public class AdminController : Controller
    3     {
    4         private IProductRepository repository;
    5 
    6         public AdminController(IProductRepository productRepository)
    7         {
    8             repository = productRepository;
    9         }

    如果将该过滤器应用到Action方法里,则只对这个Action起作用。

    创建表单验证方法

    有了表单验证配置和表单验证过滤器之后,还需要定义表单验证的逻辑方法。

     首先定义一个接口IAuthProvider。

    1 namespace SportsStore.Infrastructure.Abstract
    2 {
    3     public interface IAuthProvider
    4     {
    5         bool Authenticate(string userName, string password);
    6     }
    7 }

    该接口只定义了一个接口方法Authenticate,根据传入的用户名和密码,返回验证是否成功的布尔值。

    然后,实现接口IAuthProvider的类FormsAuthProvider 。

     1 using SportsStore.Infrastructure.Abstract;
     2 using System.Web.Security;
     3 
     4 namespace SportsStore.WebUI.Infrastructure.Concrete
     5 {
     6     public class FormsAuthProvider : IAuthProvider
     7     {
     8         public bool Authenticate(string userName, string password)
     9         {
    10             bool result = FormsAuthentication.Authenticate(userName, password);
    11             if (result)
    12             {
    13                 FormsAuthentication.SetAuthCookie(userName, false);
    14             }
    15             return result;
    16         }
    17     }
    18 }

    这里将调用静态函数FormsAuthentication.Authenticate进行表单验证。如果Web.config文件中定义了credentials配置,则使用配置文件中定义的用户名和密码进行验证。

    如果验证成功,则调用另一个静态函数FormsAuthentication.SetAuthCookie向客户端写入用户名userName字符串的cookie。

    还需要将实现类FormsAuthProvider绑定到接口IAuthProvider。

    修改类NinjectDependencyResolver的方法AddBindings,添加Ninject绑定。

     1         private void AddBindings()
     2         {
     3             kernel.Bind<IProductRepository>().To<EFProductRepository>();
     4 
     5             EmailSettings emailSettings = new EmailSettings
     6             {
     7                 WriteAsFile = bool.Parse(System.Configuration.ConfigurationManager.AppSettings["Email.WriteAsFile"] ?? "false")
     8             };
     9             kernel.Bind<IOrderProcessor>().To<EmailOrderProcessor>().WithConstructorArgument("settings", emailSettings);
    10 
    11             kernel.Bind<IAuthProvider>().To<FormsAuthProvider>();
    12         }

    定义LoginViewModel

     1 using System.ComponentModel.DataAnnotations;
     2 
     3 namespace SportsStore.WebUI.Models
     4 {
     5     public class LoginViewModel
     6     {
     7         [Display(Name = "User Name")]
     8         [Required(ErrorMessage = "Please enter a user name")]
     9         public string UserName { get; set; }
    10         [Required(ErrorMessage = "Please enter a password")]
    11         [DataType(DataType.Password)]
    12         public string Password { get; set; }
    13     }
    14 }

    这个视图模型类只有用户名和密码属性。它们都加了Required验证特性。Password属性加了DataType特性,这样自动生成的表单password输入框元素将是一个password输入框(输入的文本内容不可见)。

    创建Account控制器

     1 using SportsStore.Infrastructure.Abstract;
     2 using SportsStore.WebUI.Models;
     3 using System.Web.Mvc;
     4 
     5 namespace SportsStore.Controllers
     6 {
     7     public class AccountController : Controller
     8     {
     9         IAuthProvider authProvider;
    10 
    11         public AccountController(IAuthProvider authProvidor)
    12         {
    13             authProvider = authProvidor;
    14         }
    15 
    16         public ActionResult Login()
    17         {
    18             return View();
    19         }
    20 
    21         [HttpPost]
    22         public ActionResult Login(LoginViewModel model, string returnUrl)
    23         {
    24             if (ModelState.IsValid)
    25             {
    26                 if (authProvider.Authenticate(model.UserName, model.Password))
    27                 {
    28                     return Redirect(returnUrl ?? Url.Action("Index", "Admin"));
    29                 }
    30                 else
    31                 {
    32                     ModelState.AddModelError("", "Incorrect username or password");
    33                     return View();
    34                 }
    35             }
    36             else
    37             {
    38                 return View();
    39             }
    40         }
    41     }
    42 }

    这个控制器代码很简单。定义了两个Login的Action方法,一个用于接收Get请求,一个用于接收Post请求。

    Post请求的Login方法,还接收了一个returnUrl字符串参数。他是过滤器拦截的页面URL。

    调用authProvider.Authenticate返回表单验证结果。如果验证成功,则调用Redirect方法,将页面定向到刚才要访问的页面。

    创建登录视图

     1 @model SportsStore.WebUI.Models.LoginViewModel
     2 
     3 @{
     4     ViewBag.Title = "Admin: Login";
     5     Layout = "~/Views/Shared/_AdminLayout.cshtml";
     6 }
     7 <div class="panel">
     8     <div class="panel-heading">
     9         <h3>Log In</h3>
    10         <p class="lead">
    11             Please log in to access the administration area:
    12         </p>
    13     </div>
    14     <div class="panel-body">
    15         @using (Html.BeginForm())
    16         {
    17             <div class="panel-body">
    18                 @Html.ValidationSummary()
    19                 <div class="form-group">
    20                     <label>User Name</label>
    21                     @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
    22                 </div>
    23                 <div class="form-group">
    24                     <label>Password</label>
    25                     @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
    26                 </div>
    27                 <input type="submit" value="Log in" class="btn btn-primary" />
    28             </div>
    29         }
    30     </div>
    31 </div>        

    它是一个简单的用户名密码登录视图。调用Html帮助方法PasswordFor生成密码输入框。

    运行程序,当访问/Admin页面的时候,页面将自动跳转到/Account/Login页面,并在URL上添加?ReturnUrl=%2fAdmin后缀。

    如果输入用户名user,密码123,点击Log in按钮后,跳转到Admin页面。

     自定义表单验证逻辑

    上面的表单验证逻辑在非常简单的网站上是可以使用的。但是在真实的应用系统中,往往需要将用户名和密码记录在数据库表里,通过查询数据库验证用户名和密码是否正确。有时候,还需要在操作的时候,记录执行该操作的当前登录者。在首页上,有时候要显示当前登录者信息。下面将简单介绍这些功能怎样实现。

    首先定义数据库实体类User。

    1 namespace SportsStore.Domain.Entities
    2 {
    3     public class User
    4     {
    5         public int UserID { get; set; }
    6         public string UserName { get; set; }
    7         public string Password { get; set; }
    8     }
    9 }

    定义继承IIdentity接口的用户类UserIdentity。

     1 using SportsStore.Domain.Entities;
     2 using System.Security.Principal;
     3 
     4 namespace SportsStore.Infrastructure.Security
     5 {
     6     public class UserIdentity : IIdentity
     7     {
     8         public string AuthenticationType
     9         {
    10             get
    11             {
    12                 return "Form";
    13             }
    14         }
    15 
    16         public bool IsAuthenticated
    17         {
    18             get;
    19             //extend property
    20             set;
    21         }
    22 
    23         public string Name
    24         {
    25             get
    26             {
    27                 return User.UserName;
    28             }
    29         }
    30 
    31         public User User
    32         {
    33             get;set;
    34         }
    35     }
    36 }

    接口IIdentity的定义如下:

    继承的AutenticationType属性返回Form字符串。继承的IsAuthenticated属性,扩展了set访问器,增加了可写访问,让外部程序可以设置它的值。在实现类UserIdentity里增加了实体User类的属性。继承的Name属性返回User属性的属性Name。

    定义继承IPrincipal接口的用户类UserProfile。

     1 using System.Security.Principal;
     2 using System.Web;
     3 
     4 namespace SportsStore.Infrastructure.Security
     5 {
     6     public class UserProfile : IPrincipal
     7     {
     8         public const string SessionKey = "User";
     9 
    10         private UserIdentity _user;
    11 
    12         public IIdentity Identity
    13         {
    14             get
    15             {
    16                 return _user;
    17             }
    18             set //extended property
    19             {
    20                 _user = (UserIdentity)value;
    21             }
    22         }
    23 
    24         public bool IsInRole(string role)
    25         {
    26             return true;
    27         }
    28 
    29         public static UserProfile CurrentLogonUser
    30         {
    31             get
    32             {
    33                 if (HttpContext.Current.Session == null)
    34                 {
    35                     return null;
    36                 }
    37                 if (HttpContext.Current.Session[SessionKey] == null)
    38                 {
    39                     return null;
    40                 }
    41                 return HttpContext.Current.Session[SessionKey] as UserProfile;
    42             }
    43         }
    44     }
    45 }

    接口IPrincipal的定义如下:

    继承类UserProfile,定义了一个私有的UserIdentity类型的_user字段,通过继承的属性Identity返回它的值。继承的属性Identity扩展了它的可写访问器,让外部程序可以设置它的值。继承的方法IsInRole暂时返回true。

    在UserProfile类里还定义了一个UserProfile类型的静态属性CurrentLogonUser,他用于在应用程序的任何地方返回当前登录用户的信息。从它的代码看到,我将使用Session存储当前登录用户对象。

    修改接口IAuthProvider和类FormsAuthProvider。

     1 using SportsStore.Infrastructure.Security;
     2 using SportsStore.WebUI.Models;
     3 
     4 namespace SportsStore.Infrastructure.Abstract
     5 {
     6     public interface IAuthProvider
     7     {
     8         bool Authenticate(LoginViewModel loginModel, out UserProfile userProfile);
     9     }
    10 }

    方法Authenticate增加了一个out修饰的UserProfile参数,用于返回验证成功后的UserProfile类型对象。

    using SportsStore.Infrastructure.Abstract;
    using SportsStore.Infrastructure.Security;
    using SportsStore.WebUI.Models;
    
    namespace SportsStore.WebUI.Infrastructure.Concrete
    {
        public class FormsAuthProvider : IAuthProvider
        {
            public bool Authenticate(LoginViewModel loginModel, out UserProfile userProfile)
            {
                //validate user and password from database
                bool result = true;
                userProfile = new UserProfile();
                if (result)
                {
                    UserIdentity userIdentity = new UserIdentity();
                    userIdentity.IsAuthenticated = true;
                    // get user entity from db
                    userIdentity.User = new Domain.Entities.User()
                    {
                        UserID = 0,
                        UserName = loginModel.UserName,
                        Password = loginModel.Password
                    };
    
                    userProfile.Identity = userIdentity;
                }
                return result;
            }
        }
    }

    方法Authenticate将查询数据库验证用户名和密码,如果验证通过,将数据库读出来的用户信息生成UserProfile对象,用out参数返回。

    你可以使用Ninject注册一个IUserRepository到类FormsAuthProvider,再读数据库。

    IUserRepository接口:

     1 using SportsStore.Domain.Entities;
     2 using System.Collections.Generic;
     3 
     4 namespace SportsStore.Domain.Abstract
     5 {
     6     public interface IUserRepository
     7     {
     8         IEnumerable<User> Users { get; }
     9     }
    10 }

    实现类EFUserRepository:

     1 using SportsStore.Domain.Abstract;
     2 using SportsStore.Domain.Entities;
     3 using System.Collections.Generic;
     4 
     5 namespace SportsStore.Domain.Concrete
     6 {
     7     public class EFUserRepository : IUserRepository
     8     {
     9         private EFDbContext context = new EFDbContext();
    10         public IEnumerable<User> Users
    11         {
    12             get
    13             {
    14                 try
    15                 {
    16                     return context.Users;
    17                 }
    18                 catch (System.Exception e)
    19                 {
    20                     return null;
    21                 }
    22             }
    23         }
    24     }
    25 }

    EFDbContext:

     1 using SportsStore.Domain.Entities;
     2 using System.Data.Entity;
     3 
     4 namespace SportsStore.Domain.Concrete
     5 {
     6     public class EFDbContext : DbContext
     7     {
     8         public DbSet<Product> Products { get; set; }
     9 
    10         public DbSet<User> Users { get; set; }
    11     }
    12 }

    修改后的FormsAuthProvider:

     1 using SportsStore.Domain.Abstract;
     2 using SportsStore.Infrastructure.Abstract;
     3 using SportsStore.Infrastructure.Security;
     4 using SportsStore.WebUI.Models;
     5 using System.Linq;
     6 
     7 namespace SportsStore.WebUI.Infrastructure.Concrete
     8 {
     9     public class FormsAuthProvider : IAuthProvider
    10     {
    11         private IUserRepository _userRepository;
    12 
    13         public FormsAuthProvider(IUserRepository userRepository)
    14         {
    15             _userRepository = userRepository;
    16         }
    17 
    18         public bool Authenticate(LoginViewModel loginModel, out UserProfile userProfile)
    19         {
    20             //validate user and password from database
    21             var user = _userRepository.Users.Where(u => u.UserName == loginModel.UserName && u.Password == loginModel.Password).FirstOrDefault();
    22             bool result = user != null;
    23             userProfile = new UserProfile();
    24             if (result)
    25             {
    26                 UserIdentity userIdentity = new UserIdentity();
    27                 userIdentity.IsAuthenticated = true;
    28                 // get user entity from db
    29                 userIdentity.User = user;
    30 
    31                 userProfile.Identity = userIdentity;
    32             }
    33             return result;
    34         }
    35     }
    36 }

    类NinjectDependencyResolver里的AddBindings方法添加绑定:

    1 kernel.Bind<IUserRepository>().To<EFUserRepository>();

    还需要添加用户表Users。

    里面添加一行数据。

    添加继承自AuthorizeAttribute类的自定义Authorize特性类MyAuthorizeAttribute。

     1 using System.Web;
     2 using System.Web.Mvc;
     3 
     4 namespace SportsStore.Infrastructure.Security
     5 {
     6     public class MyAuthorizeAttribute : AuthorizeAttribute
     7     {
     8         protected override bool AuthorizeCore(HttpContextBase httpContext)
     9         {
    10             UserProfile userProfile = null;
    11             if (httpContext.Session != null)
    12             {
    13                 userProfile = (UserProfile)httpContext.Session[UserProfile.SessionKey];
    14             }
    15             if (userProfile == null)
    16             {
    17                 return false;
    18             }
    19             else
    20             {
    21                 //some other validate logic here, like intercept IP
    22                 return userProfile.Identity.IsAuthenticated;
    23             }
    24         }
    25     }
    26 }

    该类是根据Session对象存储的User对象。拦截控制器方法。

    将自定义AuthorizeAttribute特性MyAuthorizeAttribute应用到AdminController控制器。

    1     [MyAuthorize] 
    2     public class AdminController : Controller
    3     {

    最后是修改Account控制器的Login方法。

     1         [HttpPost]
     2         public ActionResult Login(LoginViewModel model, string returnUrl)
     3         {
     4             if (ModelState.IsValid)
     5             {
     6                 UserProfile userProfile;
     7                 if (authProvider.Authenticate(model, out userProfile))
     8                 {
     9                     HttpContext.Session[UserProfile.SessionKey] = userProfile;
    10                     return Redirect(returnUrl ?? Url.Action("Index", "Admin"));
    11                 }
    12                 else
    13                 {
    14                     ModelState.AddModelError("", "Incorrect username or password");
    15                     return View();
    16                 }
    17             }
    18             else
    19             {
    20                 return View();
    21             }
    22         }

    调用的authProvider.Authenticate方法增加了out参数userProfile。如果userProfile对象写入Session。

    在首页上显示当前登录用户

    在AdminController控制器里,添加LogonUser方法Action,返回包含当前登录用户对象的PartialViewResult。

    1         public PartialViewResult LogonUser()
    2         {
    3             var user = UserProfile.CurrentLogonUser != null ? UserProfile.CurrentLogonUser.Identity as UserIdentity : null;
    4             if (user != null)
    5             {
    6                 return PartialView("LogonUser", user.User);
    7             }
    8             return PartialView();
    9         }

    在_AdminLayout.cshtml视图中嵌入这个Action。

     1 <!DOCTYPE html>
     2 
     3 <html>
     4 <head>
     5     <meta name="viewport" content="width=device-width" />
     6     <link href="~/Content/bootstrap.css" rel="stylesheet" />
     7     <link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
     8     <link href="~/Content/ErrorStyles.css" rel="stylesheet" />
     9     <script src="~/Scripts/jquery-1.9.1.js"></script>
    10     <script src="~/Scripts/jquery.validate.js"></script>
    11     <script src="~/Scripts/jquery.validate.unobtrusive.js"></script>
    12     <title>@ViewBag.Title</title>
    13     <style>
    14         .navbar-right {
    15             float: right !important;
    16             margin-right: 15px;
    17             margin-left: 15px;
    18         }
    19     </style>
    20 </head>
    21 <body>
    22     <div class="navbar navbar-inverse" role="navigation">
    23         <a class="navbar-brand" href="#">
    24             <span class="hidden-xs">SPORTS STORE</span>
    25             <div class="visible-xs">SPORTS</div>
    26             <div class="visible-xs">STORE</div>
    27         </a>
    28         <span class="visible-xs">
    29             @Html.Action("LogonUser", "Admin", new { showPicture = true })
    30         </span>
    31         <span class="hidden-xs">
    32             @Html.Action("LogonUser", "Admin")
    33         </span>
    34     </div>
    35     <div>
    36         @if (TempData["message"] != null)
    37         {
    38             <div class="alert alert-success">@TempData["message"]</div>
    39         }
    40         @RenderBody()
    41     </div>
    42 </body>
    43 </html>

    为了支持移动设备,使用了响应式布局。在超小屏幕上,向视图LogonUser传入了一个动态参数showPicture。视图LogonUser将使用这个参数,决定是否显示图片。

    LogonUser视图:

     1 @model SportsStore.Domain.Entities.User
     2 @{
     3     bool showPicture = ((bool)(ViewContext.RouteData.Values["showPicture"] ?? false));
     4 }
     5 @if (!string.IsNullOrEmpty(Model.UserName))
     6 {
     7     <div class="navbar-brand navbar-right small">
     8         [<span>@Model.UserName</span>]
     9          @if (showPicture)
    10          {
    11             <a href="@Url.Action("Logout","Account")"><span class="glyphicon glyphicon-hand-right"></span></a>
    12          }
    13          else
    14          {
    15             @Html.RouteLink("Logout", new { controller = "Account", action = "Logout" }, new { @class = "navbar-link" })
    16          }
    17     </div>
    18 }

    运行程序,程序运行结果跟之前一样。登录成功后,在首页的右上角显示当前登录用户的用户名。

     超小屏幕上显示的效果是这样的:

    最后,还需要添加Action方法Logout。

    1         public ActionResult Logout()
    2         {
    3             FormsAuthentication.SignOut();
    4             HttpContext.Session.Remove(UserProfile.SessionKey);
    5             return Redirect(Url.Action("Login"));
    6         }

    Logout方法调用静态函数FormsAuthentication.SignOut,签出表单验证。从Session对象内删除了当前登录用户对象。调用Redirect函数,返回Login页面。

    最后,你可以删除Web.config文件中forms节点的credentials子节点。

    1   <system.web>
    2     <compilation debug="true" targetFramework="4.5.1" />
    3     <httpRuntime targetFramework="4.5.1" />
    4     <authentication mode="Forms">
    5       <forms loginUrl="~/Account/Login" timeout="2880" >
    6       </forms>
    7     </authentication>
    8   </system.web>
  • 相关阅读:
    vue-router基础使用
    Vue插槽
    Vue组件通信
    小程序自定义头部导航栏
    css日常积累
    移动端的无缝轮播图片
    vue的h5开发中,将页面保存为图片
    vue-webpack打包问题
    洛谷P1341 最受欢迎的奶牛
    Tarjan 算法详解
  • 原文地址:https://www.cnblogs.com/uncle_danny/p/9085035.html
Copyright © 2011-2022 走看看