丹尼大叔

数学专业毕业,爱上编程的大叔,兴趣广泛。使用博客园这个平台分享我工作和业余的学习内容,以编程交友。有朋自远方来,不亦乐乎。

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

 摘要:

在之前的文章中,我给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>

 

posted on 2018-05-24 22:20  丹尼大叔  阅读(433)  评论(0编辑  收藏  举报