ASP.NET MVC 4 (十三) 基於表單的身份驗證


在前面的章節中我們知道可以在MVC應用程序中使用[Authorize]特性來限制用戶對某些網址(控制器/控制器方法)的訪問,但這都是在對用戶認證之后,而用戶的認證則依然是使用ASP.NET平台的認證機制。

ASP.NET提供Windows和Forms兩種身份驗證,前者主要用於Intranet上域環境內,后者則更多的應用於Internet,這里我們只討論后者。先從最簡單的例子開始,我們在web.config中配置Forms認證方式:

... 
<authentication mode="Forms"> 
  <forms loginUrl="~/Account/Login" timeout="2880"> 
    <credentials passwordFormat="Clear"> 
      <user name="admin" password="secret" /> 
    </credentials> 
  </forms> 
</authentication> 
... 

這里設置認證方式為Forms,用戶登錄的地址為~/Account/Login,我們用最簡單的方式創建用戶信息,在credentials節中直接設置用戶名稱/密碼。在創建頁面之前我們先創建收集用戶名和密碼Model類:

using System.ComponentModel.DataAnnotations;

namespace SportsStore.WebUI.Models {

    public class LoginViewModel {
        [Required]
        public string UserName { get; set; }

        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
}

創建一個視圖來收集用戶名和信息:

@model SportsStore.WebUI.Models.LoginViewModel

@{
    ViewBag.Title = "Admin: Log In";
    Layout = "~/Views/Shared/_AdminLayout.cshtml";
}

<h1>Log In</h1>

<p>Please log in to access the administrative area:</p>
@using(Html.BeginForm()) {
    @Html.ValidationSummary(true)
    @Html.EditorForModel()
    <p><input type="submit" value="Log in" /></p>
}

最后還需要在Account控制器的Login action中處理用戶提交的用戶名和密碼完成用戶認證:

[HttpPost]
        public ActionResult Login(LoginViewModel model)
        {

            if (ModelState.IsValid)
            {
                bool result = FormsAuthentication.Authenticate(model.UserName, model.Password);
                if (result)
                {
                    FormsAuthentication.SetAuthCookie(model.UserName, false);
                    return Redirect(Url.Action("Index", "Admin"));
                }
                else
                {
                    ModelState.AddModelError("", "Incorrect username or password");
                    return View();
                }
            }
            else
            {
                return View();
            }
        }

調用FormsAuthentication.Authenticate()對用戶名和密碼驗證,如何驗證成功,調用FormsAuthentication.SetAuthCookie()設置用戶驗證的cookie並在響應中返回,在cookie過期之前用戶再次訪問時不再要求登錄。

以上就是最簡單的Forms身份驗證過程,但實際的Internet應用用戶的信息一般存儲在數據庫中,通過Membership provider利用數據庫中的信息對用戶驗證,MVC4中微軟為我們提供SQL membership provider、Universal membership provider和Simple membership provider,下面來看看如何具體如何使用它們。

SQL membership provider

在.NET 2.0中SQL membership provider就已經存在了,在visual studio 2012中使用empty模板創建一個MVC4的工程,web.config你不會看到任何membership provider相關的信息,默認使用的是Windows認證。在VS的Project菜單下打開Asp.net configurtion工具(在打開配置工具前記得編譯下工程,否則會提示“選定的數據存儲區出現問題”),在“安全”標簽頁面點擊“選擇身份驗證類型”,配置工具詢問“用戶如何訪問您的站點?”,選擇“通過Internet”,點擊“完成”后配置工具將自動在web.config中添加“<authentication mode="Forms" />”。配置工具仍然沒有在web.config添加任何membership provider的信息,但是我們轉到配置工具的“提供程序頁面”,可以看到看到默認選中的是AspNetSqlMembershipProvider,同時配置工具會在工程的app_data目錄下創建一個名為ASPNETDB.MDF的數據庫,這是一個sql express的數據庫,visual studio 2012中不能直接打開(VS用的是localdb),可以在SQL管理工具中附加到SQL EXPRESS的服務實例來查看。打開數據庫可以看到數據庫中添加了很多“aspnet_”為前綴的表和存儲過程,這些都是SqlMembershipProvider需要的。

如果我們要使用自建的數據庫來保存用戶信息改如何操作呢?我們在Solution exploer中點擊App_Start目錄,右鍵菜單中選擇添加->添加項目->SQL數據庫創建一個localdb的數據庫,添加相應的Connection字符串到web.config:

<connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=(localdb)\v11.0;AttachDbFileName=|DataDirectory|\mvc4empty.mdf;Initial Catalog=mvc4empty;Integrated Security=True"
      providerName="System.Data.SqlClient"/>
  </connectionStrings>

我們還需要在web.config手工添加SqlMembershipProvider,讓它使用上面的數據庫連接:

<membership defaultProvider="mySqlMembershipProvider">
      <providers>
        <clear />
        <add connectionStringName="DefaultConnection" enablePasswordRetrieval="false"
          enablePasswordReset="true" requiresQuestionAndAnswer="false"
          applicationName="/" requiresUniqueEmail="false" passwordFormat="Hashed"
          passwordStrengthRegularExpression="" name="mySqlMembershipProvider"
          type="System.Web.Security.SqlMembershipProvider" />
      </providers>
    </membership>

再次打開asp.net配置工具轉到安全界面會提示錯誤“Could not find stored procedure 'dbo.aspnet_CheckSchemaVersion'”,配置工具試圖調用相關的存儲過程,但是數據庫是我們手工創建的,不包含這些過程和數據表。我們可以使用aspnet_regsql.exe工具在我們的數據庫中創建相關表和數據,C:\Windows\Microsoft.NET\Framework64\v4.0.30319和C:\Windows\Microsoft.NET\Framework64\v2.0.50727都有這個工具,沒有細究兩個版本的不同,這里使用.NET 4.0的版本。在aspnet_regsql工具選擇服務器為“(localdb)\v11.0”,數據庫列表中如果找不到新建的數據庫,可以事先在sql manage studio中連接到服務引擎“(localdb)\v11.0”后附加該數據庫(aspnet_reqsql也支持使用連接字符串作為參數,參見http://msdn.microsoft.com/en-us/library/ms229862(v=vs.100).aspx)。完成上述操作后,asp.net配置工具就可以在我們的數據庫中創建管理用戶了。

准備好Forms認證的配置,我們繼續完善上面的例子,從控制器開始:

using System;
using System.Web.Mvc;
using System.Web.Security;
using SportsStore.WebUI.Models;

namespace SportsStore.WebUI.Controllers
{

    public class AccountController : Controller
    {
        public ViewResult Login(string returnUrl = null)
        {
            ViewBag.ReturnUrl = returnUrl;
            return View();
        }

        [HttpPost]
        public ActionResult Login(LoginViewModel model, string returnUrl)
        {
            if (!ModelState.IsValid) return View();
            var result = Membership.ValidateUser(model.UserName, model.Password);
            if (result)
            {
                FormsAuthentication.SetAuthCookie(model.UserName, false);
                return Redirect(returnUrl ?? Url.Action("Index", "Home"));
            }
            ModelState.AddModelError("", "Incorrect username or password");
            return View();
        }

        public ActionResult Logout(string returnUrl)
        {
            FormsAuthentication.SignOut();
            return Redirect(returnUrl ?? Url.Action("Index", "Home"));
        }
        
        public ViewResult Register()
        {
            return View();
        }

        [HttpPost]
        public ViewResult Register(LoginViewModel model)
        {
            if (!ModelState.IsValid) return View(model);
            try
            {
                Membership.CreateUser(model.UserName, model.Password);
                ViewBag.Registered = true;
            }
            catch (Exception exception)
            {
                ModelState.AddModelError("",exception.Message);
            }
            return View(model);
        }
    }
}

在用戶登錄時不再使用FormsAuthentication.Authenticate()認證用戶,它僅讀取web.config中credentials節的內容,我們需要改用Membership.ValidateUser()對用戶密碼校驗。調用FormsAuthentication.SignOut()登出用戶,它清除認證相關的cookie。Register() action用於創建用戶,它調用Membership.CreateUser()創建一個用戶保存到數據庫中,對應的Register視圖:

@model SportsStore.WebUI.Models.LoginViewModel

@{
    ViewBag.Title = "User: Register";
    Layout = "~/Views/Shared/_AdminLayout.cshtml";
}

<h1>User register</h1>
@if (ViewBag.Registered != null && ViewBag.Registered)
{
    <p>User "@Model.UserName" has been created sucessfully!</p>
}
else
{
    <p>Please input user name and password to register:</p>
    using (Html.BeginForm())
    {
    @Html.ValidationSummary(true)
    @Html.EditorForModel()
    <p>
        <input type="submit" value="Register" /></p>
    }
}

作為示例這里簡單的收集用戶名和密碼,成功注冊后給出提示,Html.ValidationSummary()顯示發生的錯誤發生,比如用戶名已經存在。我們可以在布局文件中創建一些鏈接關聯到用戶注冊、登出:

...
<div>
            @if (User.Identity.IsAuthenticated)
            {
                <p>Current user:@User.Identity.Name</p>
                @Html.RouteLink("Logout",new {Controller="Account",Action="Logout",returnUrl=Request.Url.PathAndQuery})
            }
            else
            {
                <span>@Html.RouteLink("Login",new {Controller="Account",Action="Login",returnUrl=Request.Url.PathAndQuery})</span>
                <span>@Html.ActionLink("Register","Register","Account")</span>
            }
        </div>
        <div>
            @if (User.IsInRole("Admin"))
            {
                @Html.ActionLink("Administrate", "Index", "Admin")
            }
        </div>
...

 

Universal provider

SQL membership provider要求使用完整安裝的SQL server,使用到很多表和存儲過程,對SQL server azure、SQL server compact都不支持,於是Universal provider出現了,最早於 2011年發布。我們可以在VS2012中使用Basic模板創建MVC4工程,工程被配置為默認使用Universal provider。我們也可以在nuget包管理器搜索“universal”,找到“Microsoft ASP.NET universal provider”安裝,安裝工具修改web.config配置DefaultMembershipProvider作為默認的provider;配置EntityFramework,universal provider使用EntityFramework完成數據庫的讀寫;創建一個SQL express的數據庫和連接字符串供universal provider使用。下面是web.config的部分內容:

...
<configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
...
<profile defaultProvider="DefaultProfileProvider">
      <providers>
        <add name="DefaultProfileProvider" type="System.Web.Providers.DefaultProfileProvider, System.Web.Providers, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" applicationName="/" />
      </providers>
    </profile>
    <membership defaultProvider="DefaultMembershipProvider">
      <providers>
        <add name="DefaultMembershipProvider" type="System.Web.Providers.DefaultMembershipProvider, System.Web.Providers, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" applicationName="/" passwordFormat="Hashed" passwordStrengthRegularExpression="" />
      </providers>
    </membership>
    <roleManager defaultProvider="DefaultRoleProvider">
      <providers>
        <add name="DefaultRoleProvider" type="System.Web.Providers.DefaultRoleProvider, System.Web.Providers, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" applicationName="/" />
      </providers>
    </roleManager>
    <!--
            If you are deploying to a cloud environment that has multiple web server instances,
            you should change session state mode from "InProc" to "Custom". In addition,
            change the connection string named "DefaultConnection" to connect to an instance
            of SQL Server (including SQL Azure and SQL  Compact) instead of to SQL Server Express.
      -->
    <sessionState mode="InProc" customProvider="DefaultSessionProvider">
      <providers>
        <add name="DefaultSessionProvider" type="System.Web.Providers.DefaultSessionStateProvider, System.Web.Providers, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" />
      </providers>
    </sessionState>
....

<entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>

...

打開asp.net配置工具,可以看到成員資格提供程序有AspNetSqlMembershipProvider和DefaultMembershipProvider供選擇,前者就是sql membership provider,我們我們這時選擇它,配置工具會把membership改為:

<membership>

defaultProvider特性被刪除,不帶任何的特性,這需要特別注意。

查看Universal provider生成的數據庫,它只包含Users、Roles、Profiles、Memberships、UsersInRoles、Applications幾個表,而且沒有任何的存儲過程,確實很多程度上簡化了數據庫模型,不再使用存儲過程操作數據,因此支持的SQL服務類型也更多。nuget包安裝工具為我們自動創建了一個數據庫,如果我們要使用原有的數據庫該怎么辦呢?我們只需要改動相應的連接字符串,編譯后啟動asp.net配置工具,它會在我們原有的數據庫中創建上面的幾個表。

SQL membership provider一節示例的的控制器/視圖我們不需要任何改動都可以在切換成universal provider后正常運行,對Membership方法的調用在MVC內部轉由System.Web.Providers.DefaultProfileProvider,對我們寫程序講沒有任何不同。這樣講似乎universal provider沒有帶來太多的好處,實際上隨着數據庫結構的簡化,對我們擴展profile等有很大的便利,這里就不再深入討論。

Simple provider

simple provider在VS 2010 SP1中隨Webmatrix發布,和universal provider一樣使用entrity framework操作用戶信息數據庫,但是數據庫的結構更為簡單也可以更為靈活的配置。在VS2012中我們使用Internet模板創建MVC4的工程,工程被配置為使用simple provider。web.config中只有<authentication mode="Forms">,不再包含membership provider的信息,membership的處理直接在控制器中使用WebMatrix.WebData.WebSecurity處理。Internet模板創建了具備完整用戶功能的代碼,這里不一一列出。

Internet模板創建一個名為InitializeSimpleMembershipAttribute的過濾器,它在每次應用程序啟動時調用一次:

WebSecurity.InitializeDatabaseConnection("DefaultConnection", "UserProfile", "UserId", "UserName", autoCreateTables: true);

這個方法使初始化用戶信息的數據庫連接,DefaultConnection為數據庫的連接字符串,Userpofile為表名稱,UserId和UserName分別為用戶ID和用戶名稱在表中的字段名稱,也就是說我們只需要一個最簡單的有用戶ID和名稱兩個字段的表就可以了,這個表可以在任何數據庫中,這是可以動態設置的,所以asp.net的配置工具不能用於配置simple provider。

Internet模板創建Account控制器,包含眾多action方法用於提供用戶注冊、登錄、登出、修改密碼,基本上都是調用WebSecurity的相關方法來實現的,比如登錄調用的是WebSecurity.Login()。在Internet模板的基礎上,我們可以很方便的自定義profile、roles等,這里也不再深入,已經有一篇很好的文章講解simple provider如何工作,可以參見http://weblogs.asp.net/jgalloway/archive/2012/08/29/simplemembership-membership-providers-universal-providers-and-the-new-asp-net-4-5-web-forms-and-asp-net-mvc-4-templates.aspx

 

MVC5已隨VS2013在2013十月發布,相對於MVC4有了很多的變化,包括這里所講的安全認證。就以本文結束MVC4,開始MVC5之旅。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM