摘要:
在之前的文章中,我给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>
作者:丹尼大叔
出处:http://www.cnblogs.com/uncle_danny
本文版权归作者和博客园共有,欢迎转载。但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。