摘要:

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

创建基本安全策略

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

   <system.web>
<compilation debug="true" targetFramework="4.5.1" />
<httpRuntime targetFramework="4.5.1" />
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" timeout="2880" >
</forms>
</authentication>
</system.web>

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

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

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

   <system.web>
<compilation debug="true" targetFramework="4.5.1" />
<httpRuntime targetFramework="4.5.1" />
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" timeout="2880" >
<credentials passwordFormat="Clear">
<user name="user" password="123"/>
</credentials>
</forms>
</authentication>
</system.web>

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

使用过滤器应用表单验证

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

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

     [Authorize]
public class AdminController : Controller
{
private IProductRepository repository; public AdminController(IProductRepository productRepository)
{
repository = productRepository;
}

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

创建表单验证方法

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

首先定义一个接口IAuthProvider。

 namespace SportsStore.Infrastructure.Abstract
{
public interface IAuthProvider
{
bool Authenticate(string userName, string password);
}
}

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

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

 using SportsStore.Infrastructure.Abstract;
using System.Web.Security; namespace SportsStore.WebUI.Infrastructure.Concrete
{
public class FormsAuthProvider : IAuthProvider
{
public bool Authenticate(string userName, string password)
{
bool result = FormsAuthentication.Authenticate(userName, password);
if (result)
{
FormsAuthentication.SetAuthCookie(userName, false);
}
return result;
}
}
}

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

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

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

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

         private void AddBindings()
{
kernel.Bind<IProductRepository>().To<EFProductRepository>(); EmailSettings emailSettings = new EmailSettings
{
WriteAsFile = bool.Parse(System.Configuration.ConfigurationManager.AppSettings["Email.WriteAsFile"] ?? "false")
};
kernel.Bind<IOrderProcessor>().To<EmailOrderProcessor>().WithConstructorArgument("settings", emailSettings); kernel.Bind<IAuthProvider>().To<FormsAuthProvider>();
}

定义LoginViewModel

 using System.ComponentModel.DataAnnotations;

 namespace SportsStore.WebUI.Models
{
public class LoginViewModel
{
[Display(Name = "User Name")]
[Required(ErrorMessage = "Please enter a user name")]
public string UserName { get; set; }
[Required(ErrorMessage = "Please enter a password")]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

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

创建Account控制器

 using SportsStore.Infrastructure.Abstract;
using SportsStore.WebUI.Models;
using System.Web.Mvc; namespace SportsStore.Controllers
{
public class AccountController : Controller
{
IAuthProvider authProvider; public AccountController(IAuthProvider authProvidor)
{
authProvider = authProvidor;
} public ActionResult Login()
{
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("", "Incorrect username or password");
return View();
}
}
else
{
return View();
}
}
}
}

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

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

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

创建登录视图

 @model SportsStore.WebUI.Models.LoginViewModel

 @{
ViewBag.Title = "Admin: Login";
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<div class="panel">
<div class="panel-heading">
<h3>Log In</h3>
<p class="lead">
Please log in to access the administration area:
</p>
</div>
<div class="panel-body">
@using (Html.BeginForm())
{
<div class="panel-body">
@Html.ValidationSummary()
<div class="form-group">
<label>User Name</label>
@Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
</div>
<div class="form-group">
<label>Password</label>
@Html.PasswordFor(m => m.Password, new { @class = "form-control" })
</div>
<input type="submit" value="Log in" class="btn btn-primary" />
</div>
}
</div>
</div>

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

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

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

 自定义表单验证逻辑

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

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

 namespace SportsStore.Domain.Entities
{
public class User
{
public int UserID { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
}
}

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

 using SportsStore.Domain.Entities;
using System.Security.Principal; namespace SportsStore.Infrastructure.Security
{
public class UserIdentity : IIdentity
{
public string AuthenticationType
{
get
{
return "Form";
}
} public bool IsAuthenticated
{
get;
//extend property
set;
} public string Name
{
get
{
return User.UserName;
}
} public User User
{
get;set;
}
}
}

接口IIdentity的定义如下:

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

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

 using System.Security.Principal;
using System.Web; namespace SportsStore.Infrastructure.Security
{
public class UserProfile : IPrincipal
{
public const string SessionKey = "User";

private UserIdentity _user; public IIdentity Identity
{
get
{
return _user;
}
set //extended property
{
_user = (UserIdentity)value;
}
} public bool IsInRole(string role)
{
return true;
} public static UserProfile CurrentLogonUser
{
get
{
if (HttpContext.Current.Session == null)
{
return null;
}
if (HttpContext.Current.Session[SessionKey] == null)
{
return null;
}
return HttpContext.Current.Session[SessionKey] as UserProfile;
}
}
}
}

接口IPrincipal的定义如下:

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

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

修改接口IAuthProvider和类FormsAuthProvider。

 using SportsStore.Infrastructure.Security;
using SportsStore.WebUI.Models; namespace SportsStore.Infrastructure.Abstract
{
public interface IAuthProvider
{
bool Authenticate(LoginViewModel loginModel, out UserProfile userProfile);
}
}

方法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 = ,
UserName = loginModel.UserName,
Password = loginModel.Password
}; userProfile.Identity = userIdentity;
}
return result;
}
}
}

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

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

IUserRepository接口:

 using SportsStore.Domain.Entities;
using System.Collections.Generic; namespace SportsStore.Domain.Abstract
{
public interface IUserRepository
{
IEnumerable<User> Users { get; }
}
}

实现类EFUserRepository:

 using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using System.Collections.Generic; namespace SportsStore.Domain.Concrete
{
public class EFUserRepository : IUserRepository
{
private EFDbContext context = new EFDbContext();
public IEnumerable<User> Users
{
get
{
try
{
return context.Users;
}
catch (System.Exception e)
{
return null;
}
}
}
}
}

EFDbContext:

 using SportsStore.Domain.Entities;
using System.Data.Entity; namespace SportsStore.Domain.Concrete
{
public class EFDbContext : DbContext
{
public DbSet<Product> Products { get; set; } public DbSet<User> Users { get; set; }
}
}

修改后的FormsAuthProvider:

 using SportsStore.Domain.Abstract;
using SportsStore.Infrastructure.Abstract;
using SportsStore.Infrastructure.Security;
using SportsStore.WebUI.Models;
using System.Linq;

namespace SportsStore.WebUI.Infrastructure.Concrete
{
public class FormsAuthProvider : IAuthProvider
{
private IUserRepository _userRepository; public FormsAuthProvider(IUserRepository userRepository)
{
_userRepository = userRepository;
}

public bool Authenticate(LoginViewModel loginModel, out UserProfile userProfile)
{
//validate user and password from database
var user = _userRepository.Users.Where(u => u.UserName == loginModel.UserName && u.Password == loginModel.Password).FirstOrDefault();
bool result = user != null;
userProfile = new UserProfile();
if (result)
{
UserIdentity userIdentity = new UserIdentity();
userIdentity.IsAuthenticated = true;
// get user entity from db
userIdentity.User = user;

userProfile.Identity = userIdentity;
}
return result;
}
}
}

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

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

还需要添加用户表Users。

里面添加一行数据。

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

 using System.Web;
using System.Web.Mvc; namespace SportsStore.Infrastructure.Security
{
public class MyAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
UserProfile userProfile = null;
if (httpContext.Session != null)
{
userProfile = (UserProfile)httpContext.Session[UserProfile.SessionKey];
}
if (userProfile == null)
{
return false;
}
else
{
//some other validate logic here, like intercept IP
return userProfile.Identity.IsAuthenticated;
}
}
}
}

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

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

     [MyAuthorize]
public class AdminController : Controller
{

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

         [HttpPost]
public ActionResult Login(LoginViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
UserProfile userProfile;
if (authProvider.Authenticate(model, out userProfile))
{
HttpContext.Session[UserProfile.SessionKey] = userProfile;
return Redirect(returnUrl ?? Url.Action("Index", "Admin"));
}
else
{
ModelState.AddModelError("", "Incorrect username or password");
return View();
}
}
else
{
return View();
}
}

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

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

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

         public PartialViewResult LogonUser()
{
var user = UserProfile.CurrentLogonUser != null ? UserProfile.CurrentLogonUser.Identity as UserIdentity : null;
if (user != null)
{
return PartialView("LogonUser", user.User);
}
return PartialView();
}

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

 <!DOCTYPE html>

 <html>
<head>
<meta name="viewport" content="width=device-width" />
<link href="~/Content/bootstrap.css" rel="stylesheet" />
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
<link href="~/Content/ErrorStyles.css" rel="stylesheet" />
<script src="~/Scripts/jquery-1.9.1.js"></script>
<script src="~/Scripts/jquery.validate.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.js"></script>
<title>@ViewBag.Title</title>
<style>
.navbar-right {
float: right !important;
margin-right: 15px;
margin-left: 15px;
}
</style>
</head>
<body>
<div class="navbar navbar-inverse" role="navigation">
<a class="navbar-brand" href="#">
<span class="hidden-xs">SPORTS STORE</span>
<div class="visible-xs">SPORTS</div>
<div class="visible-xs">STORE</div>
</a>
<span class="visible-xs">
@Html.Action("LogonUser", "Admin", new { showPicture = true })
</span>
<span class="hidden-xs">
@Html.Action("LogonUser", "Admin")
</span>
</div>
<div>
@if (TempData["message"] != null)
{
<div class="alert alert-success">@TempData["message"]</div>
}
@RenderBody()
</div>
</body>
</html>

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

LogonUser视图:

 @model SportsStore.Domain.Entities.User
@{
bool showPicture = ((bool)(ViewContext.RouteData.Values["showPicture"] ?? false));
}
@if (!string.IsNullOrEmpty(Model.UserName))
{
<div class="navbar-brand navbar-right small">
[<span>@Model.UserName</span>]
@if (showPicture)
{
<a href="@Url.Action("Logout","Account")"><span class="glyphicon glyphicon-hand-right"></span></a>
}
else
{
@Html.RouteLink("Logout", new { controller = "Account", action = "Logout" }, new { @class = "navbar-link" })
}
</div>
}

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

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

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

         public ActionResult Logout()
{
FormsAuthentication.SignOut();
HttpContext.Session.Remove(UserProfile.SessionKey);
return Redirect(Url.Action("Login"));
}

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

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

   <system.web>
<compilation debug="true" targetFramework="4.5.1" />
<httpRuntime targetFramework="4.5.1" />
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" timeout="2880" >
</forms>
</authentication>
</system.web>

跟我学ASP.NET MVC之十:SportsStrore安全的更多相关文章

  1. 跟我学ASP.NET MVC之五:SportsStrore开始

    摘要: 这篇文章将介绍一个ASP.NET应用程序SportsStore的开发过程. 开始 创建解决方案 创建工程 在New ASP.NET Project - SportsStore窗口中,选择Emp ...

  2. 跟我学ASP.NET MVC之八:SportsStrore移动设备

    摘要: 现在的web程序开发避免不了智能手机和平板电脑上的使用,如果你希望发布你的应用程序给更广大客户使用的话,你将要拥抱可移动web浏览器的世界.向移动设备用户发布一个好的使用体验是很困难的-比只是 ...

  3. 跟我学ASP.NET MVC之六:SportsStrore添加产品目录导航

    摘要: 上一篇文章,我建立了SportsStore应用程序的核心架构.现在我将使用这个架构向这个应用程序添加功能,你将开始看到这个基础架构的作用.我将添加重要的面向客户的简单功能,在这个过程中,你将看 ...

  4. 跟我学ASP.NET MVC之七:SportsStrore一个完整的购物车

    摘要: SportsStore应用程序进展很顺利,但是我不能销售产品直到设计了一个购物车.在这篇文章里,我就将创建一个购物车. 在目录下的每个产品旁边添加一个添加到购物车按钮.点击这个按钮将显示客户到 ...

  5. [转]我要学ASP.NET MVC 3.0(十二): MVC 3.0 使用自定义的Html控件

    本文转自:http://www.cnblogs.com/lukun/archive/2011/08/05/2128693.html 概述   在ASP.NET MVC框架中已经封装了很多基于Html标 ...

  6. 跟我学ASP.NET MVC之三:完整的ASP.NET MVC程序-PartyInvites

    摘要: 在这篇文章中,我将在一个例子中实际地展示MVC. 场景 假设一个朋友决定举办一个新年晚会,她邀请我创建一个用来邀请朋友参加晚会的WEB程序.她提出了四个注意的需求: 一个首页展示这个晚会 一个 ...

  7. 跟我学ASP.NET MVC之二:第一个ASP.NET MVC程序

    摘要: 本篇文章带你一步一步创建一个简单的ASP.NET MVC程序.  创建新ASP.NET MVC工程 点击“OK”按钮后,打开下面的窗口: 这里选择“Empty”模板以及“MVC”选项.这次不创 ...

  8. 跟我学ASP.NET MVC之一:开篇有益

    摘要: ASP.NET MVC是微软的Web开发框架,结合了模型-视图-控制器(MVC)架构的有效性和整洁性,敏捷开发最前沿的思想和技术,以及现存的ASP.NET平台最好的部分.它是传统ASP.NET ...

  9. ASP.NET MVC 4 (十) 模型验证

    模型验证是在模型绑定时检查从HTTP请求接收的数据是否合规以保证数据的有效性,在收到无效数据时给出提示帮助用户纠正错误的数据. 显式模型验证 验证数据最直接的方式就是在action方法中对接收的数据验 ...

随机推荐

  1. 安装GDB和GDBSERVER

    安装GDB和GDBSERVER 转自http://www.360doc.com/content/10/0407/17/155970_21971613.shtml 把GDBSERVER装入文件系统 转自 ...

  2. [TypeStyle] Use TypeStyle keyframes to create CSS animations

    We cover CSS keyframes and how to create them using TypeStyle. We then show how to use the keyframes ...

  3. arm Linux 如何自动检测并mount SD卡,以及如何得知已经mount

    一.土八路做法: SD 卡一旦插入系统,内核会自动在/dev/下创建设备文件:sdcard. 但有时可能时用户在拨出卡前并没有umount的话,第二次插卡进去后系统创建的就不是sdcard设备文件了, ...

  4. hadoop一些常见报错的解决方式

    Failed to set setXIncludeAware(true) for parser 遇到此问题通常是jar包冲突的问题. 一种情况是我们向java的lib文件夹加入我们自己的jar包导致h ...

  5. jquery修改获取radio的选中项

    <input id="txtBeginDate" onclick="$('#divDate').css({'top':$('#txtBeginDate').offs ...

  6. svn: E200033: database is locked解决办法

    svn执行update,却被告知database is locked! 执行 svn update,却抛出个错误警报: svn: E200033: database is locked, execut ...

  7. IT忍者神龟之Hibernat持久化对象-数据表映射配置回想

    1.持久化对象POJO编写规则: 1) 有空參public构造器: 2) 提供标识属性.映射数据表主键: 3) 属性提供setter和getter方法. 4) 属性使用基本数据类型的包装类型.基本类型 ...

  8. ios 第一篇文章-xcode6.2键盘调不出来

    ios 第一篇文章 不晓得有没有人遇到过ios代码内调用键盘(keyboard)调不出来的情况,反正我是遇到了,按官方文档的说法调用键盘事件非常easy事实上: 我用了之后,不晓得为嘛,键盘就是不显示 ...

  9. 【hdu 2594】Simpsons’ Hidden Talents

    Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Total Submission(s) ...

  10. Spring MVC 请求路径遇到的302问题的解决方法

    302 Found 请求的资源现在临时从不同的URI响应请求.由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求.只有在Cache-Control或Expires中进行了指定的情况下,这 ...