65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET MVC 5:构建你的第一个 Web 应用程序 - 第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (21投票s)

2016 年 7 月 1 日

CPOL

8分钟阅读

viewsIcon

67416

downloadIcon

1425

本文介绍如何在 ASP.NET MVC 5 应用程序中创建登录页面并实现自定义的基于角色的页面授权。

引言

本文是我上一篇文章“ASP.NET MVC 5:构建您的第一个 Web 应用程序 - 第 1 部分”的续篇。

您将学到什么

  • 创建登录页面,使用表单身份验证来验证和认证用户
  • 使用自定义的授权过滤器创建自定义的基于角色的页面授权

在本演示中,我将展示如何通过实现自定义身份验证和基于角色的页面授权来创建一个简单的登录表单,而不使用 ASP.NET Membership 或 ASP.NET Identity。如果您想构建一个允许用户使用 Facebook、Twitter、Google Plus 等社交媒体帐户登录的应用程序,那么您可能需要考虑使用 ASP.NET Identity。

请注意,我不会详细介绍模型、视图和控制器函数的细节,因此在继续之前,我建议您先阅读我的上一篇文章“ASP.NET MVC 5:构建您的第一个 Web 应用程序 - 第 1 部分”,特别是如果您是 ASP.NET MVC Web 开发新手。

在我们开始动手之前,先来聊聊安全性。

表单身份验证概述

安全性是任何 Web 应用程序不可或缺的一部分。目前大多数网站都严重依赖身份验证和授权来保护其应用程序。您可以将网站类比为公司办公室,办公室对申请人或信使等人员开放,但设施的某些区域,如工作站和会议室,只有具备特定凭证的员工才能进入。例如,当您构建一个接受用户信用卡信息用于支付并将其存储到数据库中的购物车应用程序时。ASP.NET 通过提供身份验证和授权来保护您的数据库免遭公共访问。有关更多信息,请阅读:ASP.NET Web 应用程序安全基础

表单身份验证允许您使用自己的代码来认证用户,然后在 cookie 或页面 URL 中维护一个身份验证令牌。要使用表单身份验证,您需要创建一个登录页面,该页面收集用户凭据,并包含验证凭据的代码。通常,当用户尝试访问受保护的资源(例如需要身份验证的页面)时,您会将应用程序配置为重定向到登录页面。如果用户凭据有效,您可以调用 FormsAuthentication 类的各种方法,将请求重定向回原始请求的资源,并附带适当的身份验证票证(cookie)。您可以在此处阅读有关表单身份验证的更多信息。

让我们动手吧!

作为回顾,以下是之前的项目结构

图 1:项目解决方案结构

实现登录页面

启用表单身份验证

要允许表单身份验证,您应用程序中需要做的第一件事是在 web.config 文件中 FormsAuthentication<system.web> 元素下添加 <authentication><forms> 元素,如下所示。FormsAuthentication 管理您 Web 应用程序的表单身份验证服务。

<system.web>  
    <authentication mode="Forms">  
      <forms loginUrl="~/Account/Login" defaultUrl="~/Home/Welcome"></forms>  
    </authentication>  
</system.web> 

设置 loginUrl 使应用程序能够确定在用户尝试访问安全页面时将重定向到哪里。defaultUrl 在成功登录后将用户重定向到指定的页面。

添加 UserLoginView 模型

让我们通过在“UserModel”类中添加以下代码来为登录页面创建一个模型视图类。

public class UserLoginView  
{  
    [Key]  
    public int SYSUserID { get; set; }  
    [Required(ErrorMessage = "*")]  
    [Display(Name = "Login ID")]  
    public string LoginName { get; set; }  
    [Required(ErrorMessage = "*")]  
    [DataType(DataType.Password)]  
    [Display(Name = "Password")]  
    public string Password { get; set; }  
}  

上面定义的字段将用于我们的登录页面。您还可以看到字段已用 Required、Display 和 DataType 属性进行装饰。同样,这些属性被称为数据注解

添加 GetUserPassword() 方法

为“UserManager”类添加以下代码

public string GetUserPassword(string loginName) {  
            using (DemoDBEntities db = new DemoDBEntities()) {  
                var user = db.SYSUsers.Where(o => o.LoginName.ToLower().Equals(loginName));  
                if (user.Any())  
                    return user.FirstOrDefault().PasswordEncryptedText;  
                else  
                    return string.Empty;  
            }  
} 

正如方法名称所示,它使用 LINQ 查询从数据库中获取特定登录名的相应密码。

添加 Login Action 方法

为“AccountController”类添加以下代码

public ActionResult LogIn() {  
            return View();  
}  
  
[HttpPost]  
public ActionResult LogIn(UserLoginView ULV, string returnUrl) {  
            if (ModelState.IsValid) {  
                UserManager UM = new UserManager();  
                string password = UM.GetUserPassword(ULV.LoginName);  
  
                if (string.IsNullOrEmpty(password))  
                    ModelState.AddModelError("", "The user login or password provided is incorrect.");  
                else {  
                    if (ULV.Password.Equals(password)) {  
                        FormsAuthentication.SetAuthCookie(ULV.LoginName, false);  
                        return RedirectToAction("Welcome", "Home");  
                    }  
                    else {  
                        ModelState.AddModelError("", "The password provided is incorrect.");  
                    }  
                }  
            }  
  
            // If we got this far, something failed, redisplay form  
            return View(ULV);  
}  

如您所见,上面有两个同名的方法。第一个是“Login”方法,它仅返回 LogIn.cshtml 视图。我们将在下一步创建此视图。第二个方法也命名为“Login”,但它已用“[HttpPost]”属性装饰。此属性指定“Login”方法的重载,该重载只能为 POST 请求调用。

一旦触发“LogIn”按钮,就会调用第二个方法。它首先检查是否提供了必需的字段,因此它会检查 ModelState.IsValid 条件。然后,它将创建 UserManager 类的实例,并通过传递用户 LoginName 值(由用户提供)来调用 GetUserPassword() 方法。如果密码返回空字符串,则会在视图中显示错误。如果提供的密码与从数据库检索到的密码相等,则会将用户重定向到“Welcome”页面,否则会显示一个错误,指出提供的登录名或密码无效。

添加 Login 视图

在添加视图之前,请确保先构建应用程序以确保应用程序没有错误。成功生成后,导航到“AccountController”类,右键单击 Login 操作方法,然后选择“添加视图”。这将弹出下面的对话框。

图 2:添加视图对话框

请注意为每个字段提供的值。现在单击“添加”,让 Visual Studio 为您生成 UI。这是修改后的 HTML 标记。

@model MVC5RealWorld.Models.ViewModel.UserLoginView  
  
@{  
    ViewBag.Title = "LogIn";  
    Layout = "~/Views/Shared/_Layout.cshtml";  
}  
  
<h2>LogIn</h2>  
  
@using (Html.BeginForm())   
{  
    @Html.AntiForgeryToken()  
      
    <div class="form-horizontal">  
        <hr />  
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })  
        <div class="form-group">  
            @Html.LabelFor(model => model.LoginName, htmlAttributes: new { @class = "control-label col-md-2" })  
            <div class="col-md-10">  
                @Html.EditorFor(model => model.LoginName, new { htmlAttributes = new { @class = "form-control" } })  
                @Html.ValidationMessageFor(model => model.LoginName, "", new { @class = "text-danger" })  
            </div>  
        </div>  
  
        <div class="form-group">  
            @Html.LabelFor(model => model.Password, htmlAttributes: new { @class = "control-label col-md-2" })  
            <div class="col-md-10">  
                @Html.EditorFor(model => model.Password, new { htmlAttributes = new { @class = "form-control" } })  
                @Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-danger" })  
            </div>  
        </div>  
  
        <div class="form-group">  
            <div class="col-md-offset-2 col-md-10">  
                <input type="submit" value="Login" class="btn btn-default" />  
            </div>  
        </div>  
    </div>  
}  
  
<div>  
    @Html.ActionLink("Back to Main", "Index", "Home")  
</div> 

实现注销功能

注销代码非常简单。只需在 AccountController 类中添加以下方法。

[Authorize]  
public ActionResult SignOut() {  
            FormsAuthentication.SignOut();  
            return RedirectToAction("Index", "Home");  
}  

FormsAuthentication.SignOut 方法会从浏览器中删除表单身份验证票证。注销后,我们将用户重定向到“Index”页面。

这是您可以在“Home”页面中添加的“Logout”的相应操作链接。

@Html.ActionLink("Signout","SignOut","Account") 

输出

运行应用程序应显示类似以下内容。

验证触发时

图 3:验证触发

成功登录后

图 4:成功登录

注销后

图 5:注销后

很简单!现在让我们看看如何实现简单的基于角色的页面授权。

实现简单的基于角色的页面授权

授权是一个指定对某个资源或页面的访问权限的功能。一个实际的例子是有一个只有特定用户角色才能访问的页面。例如,只允许管理员访问您应用程序的维护页面。在本节中,我们将对此进行简单的实现。

创建 UserIsInRole 方法

将以下代码添加到“UserManager”类

public bool IsUserInRole(string loginName, string roleName) {  
            using (DemoDBEntities db = new DemoDBEntities()) {  
                SYSUser SU = db.SYSUsers.Where(o => o.LoginName.ToLower().Equals(loginName))?.FirstOrDefault();  
                if (SU != null) {  
                    var roles = from q in db.SYSUserRoles  
                                join r in db.LOOKUPRoles on q.LOOKUPRoleID equals r.LOOKUPRoleID  
                                where r.RoleName.Equals(roleName) && q.SYSUserID.Equals(SU.SYSUserID)  
                                select r.RoleName;  
  
                    if (roles != null) {  
                        return roles.Any();  
                    }  
                }  
  
                return false;  
            }  
}  

上述方法将 loginNameroleName 作为参数。它会检查用户表中的现有记录,然后验证是否为相应的用户分配了角色。

创建自定义授权属性过滤器

如果您还记得,我们使用 [Authorize] 属性来限制匿名用户访问某个操作方法。[Authorize] 属性为用户和角色提供了过滤器,如果您使用 Membership Provider,则很容易实现。由于我们使用自己的数据库来存储用户和角色,因此我们需要通过扩展 AuthorizeAttribute 类来实现自己的授权过滤器。

AuthorizeAttribute 指定对控制器或操作方法的访问仅限于满足授权要求的用户。我们的目标是仅基于用户角色进行页面授权。如果您想实现自定义过滤器来执行特定任务并重视关注点分离,那么您可能需要查看 IAutenticationFilter

首先,添加一个新文件夹并将其命名为“Security”。然后添加“AuthorizeRoleAttribute”类。以下是结构截图。

图 6:项目结构

这是我们自定义过滤器的代码块。

using System.Web;  
using System.Web.Mvc;  
using MVC5RealWorld.Models.DB;  
using MVC5RealWorld.Models.EntityManager;  
  
namespace MVC5RealWorld.Security  
{  
    public class AuthorizeRolesAttribute : AuthorizeAttribute  
    {  
        private readonly string[] userAssignedRoles;  
        public AuthorizeRolesAttribute(params string[] roles) {  
            this.userAssignedRoles = roles;  
        }  
        protected override bool AuthorizeCore(HttpContextBase httpContext) {  
            bool authorize = false;  
            using (DemoDBEntities db = new DemoDBEntities()) {  
                UserManager UM = new UserManager();  
                foreach (var roles in userAssignedRoles) {  
                    authorize = UM.IsUserInRole(httpContext.User.Identity.Name, roles);  
                    if (authorize)  
                        return authorize;  
                }  
            }  
            return authorize;  
        }  
        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) {  
            filterContext.Result = new RedirectResult("~/Home/UnAuthorized");  
        }  
    }  
}  

在上面的类中有两个主要方法被我们覆盖了。AuthorizeCore() 方法是身份验证检查的入口点。在这里,我们检查分配给特定用户的角色,并返回一个结果,指明用户是否被允许访问页面。HandleUnuathorizedRequest() 是一个我们将未经授权的用户重定向到的方法——“UnAuthorized”页面。

添加 AdminOnly 和 UnAuthorized 页面

现在切换回“HomeController”并添加以下代码。

[AuthorizeRoles("Admin")]  
public ActionResult AdminOnly() {  
       return View();  
}  
  
public ActionResult UnAuthorized() {  
       return View();  
}

如果您注意到,我们通过传递“Admin”作为角色名来修饰 AdminOnly 操作,并使用我们自定义的授权过滤器。这意味着只有管理员用户才能访问 AdminOnly 页面。要支持多个角色访问,只需添加另一个角色名,用逗号分隔,例如 [AuthorizeRoles(“Admin”,”Manager”)]。请注意,“Admin”和“Manager”的值应与数据库中的角色名匹配。最后,在使用 AuthorizeRoles 属性之前,请务必引用下面的命名空间。

using MVC5RealWorld.Security; 

这是 AdminOnly.cshtml 视图。

@{  
    ViewBag.Title = "AdminOnly";  
    Layout = "~/Views/Shared/_Layout.cshtml";  
}  
  
<h2>For Admin users only!</h2>  
And here’s the UnAuthorized.cshtml view:  
@{  
    ViewBag.Title = "UnAuthorized";  
    Layout = "~/Views/Shared/_Layout.cshtml";  
}  

这是 UnAuthorized.cshtml 视图。

@{  
    ViewBag.Title = "UnAuthorized";  
    Layout = "~/Views/Shared/_Layout.cshtml";  
}  
  
<h2>Unauthorized Access!</h2>  
<p>Oops! You don't have permission to access this page.</p>  
  
<div>  
    @Html.ActionLink("Back to Main", "Welcome", "Home")  
</div>

测试功能

在我们测试功能之前,先在数据库中添加一个管理员用户。在本演示中,我向数据库插入了以下数据。

INSERT INTO SYSUser (LoginName,PasswordEncryptedText, RowCreatedSYSUserID, RowModifiedSYSUserID)  
VALUES ('Admin','Admin',1,1)  
GO  

INSERT INTO SYSUserProfile (SYSUserID,FirstName,LastName,Gender,RowCreatedSYSUserID, RowModifiedSYSUserID)  
VALUES (2,'Vinz','Durano','M',1,1)  
GO  
  
INSERT INTO SYSUserRole (SYSUserID,LOOKUPRoleID,IsActive,RowCreatedSYSUserID, RowModifiedSYSUserID)  
VALUES (2,1,1,1,1)  

好的,现在我们有了要测试的数据,并且可以运行应用程序了。

输出

以下是我测试期间捕获的一些截图。

当以普通用户身份登录并访问以下 URL 时:https://:15599/Home/AdminOnly

图 7:未经授权的访问

当以管理员用户身份登录并访问以下 URL 时:https://:15599/Home/AdminOnly

图 8:管理员页面

在本系列的下一部分中,我们将学习如何在我们的 MVC 5 应用程序中执行获取、编辑、更新和删除(FEUD)操作。您可以在此处查看下一部分:ASP.NET MVC 5:构建您的第一个 Web 应用程序 - 第 3 部分

就是这样!希望您觉得这篇文章有用。

摘要

在本系列中,我们学习了如何在 ASP.NET MVC 5 应用程序中实现简单的登录页面以及如何集成自定义的基于角色的页面授权。

© . All rights reserved.