2014/1/6

[ASP.NET] Visual Studio 2013 的 Form 驗證

(本文適合已熟悉 ASP.NET 表單驗證和 Entity Framework 的開發者, 而非新手開發者)

從 Visual Studio 2013 / .Net Framework 4.5.1 開始, 關於使用者驗證這件事, 可以說做了革命性的改變。大家在過去 12 年來再熟悉不過的表單驗證方式, 在這個版本中再也沒辦法使用 (或者更正確地說, 是沒辦法延用過去的做法)。相反地, 我發現它對 AD 認證的支援變得異常地簡單; 如果你在建立專案時選擇 Windows 驗證, 那麼你的網站自動可以抓取使用者的網域登入帳號, 不必再寫任何程式。如果你要寫的是企業內部網站的話, 就變得相當容易了。

至於新的表單驗證方式, 我們會面臨的最大的困難在於不熟悉, 以及資訊太少(未來會慢慢變多, 這是確定的)。其實我是直到最近一個專案中需要用到表單驗證, 才突然驚覺它被改掉了。但是兩個多禮拜下來, 我覺得它其實基本上和原來的表單驗證方式並沒有太多的差異。你只要了解它的運作原理, 就會逐漸習慣。

不過, 容我先從兩個比較不重要的角度切入, 如此, 我在後面的文章裡就不需要再談這兩個主題。第一, 它的程式裡到處充滿著 ASYNC 和 AWAIT 關鍵字; 這代表它完全運用了 .Net 4.0 之後引進的非同步技術。不過為求簡化, 我在我的程式裡不使用非同步技術。

其次, 也許跟它大量使用非同步技術有關係, 它的執行效率似乎不佳。我發現光是要從使用者資料庫中取出十幾個使用者帳戶, 就能造成感覺得出來的延遲。這種現象在過去是前所未見的。不過話說回來, 我還沒有時間去仔細評估到底它的延遲是發生在哪一段, 所以也有可能是因為我採用的方式不對, 或是不好。但是如果你也發現類似的問題, 那麼我要建議你多多採用 Cache 以增進效能。

概觀

自從 .Net Framework 4.5 之後, 它加入了 Windows Identity Foundation (WIF) 這個新的身分認證框架。在這個框架中, 它採用了一種叫做 "Claims-based Security" (抱歉, 我不知道應該如何翻譯這個名詞, 不過, 大致上有「憑券入場」的意味) 的認證方式。每個登入者可以持有多張票券 (Claims, 其實體也可以叫做 Tokens。使用者被賦予的「角色」或者「群組」也記錄在一張 Claim 裡), 這些票券由信任的發行者 (Issuer, 一般稱為 security token service 或者 STS, 例如 Active Directory) 所發出。我們可以在「Claims-based identity term definitions」網頁中查到許多相關的名詞定義。

使用 "Claims-based Security" 的最大好處在於它可以比較輕鬆地將應用程式和認證程式二者脫鈎 (decouple, 解耦合)。同時, 透過此種技術, ASP.NET 可以支援 OAuth (例如以 FaceBook 帳號登入)。

在這個版本中, 我們可以透過 ClaimsPrincipal 取得登入資料:

System.Security.Claims.ClaimsPrincipal principal = HttpContext.Current.User
    as System.Security.Claims.ClaimsPrincipal;
lbIdentity.Text = principal.Identity.Name;

不過, 我們還是可以繼續使用 Thread.CurrentPrincipal.Identity 取到同樣型別的 Identity 物件。這是因為 ClaimsPrincipal 同樣實作了 IPrincipal 介面, 所以我們實際上是透過 IPrincipal 來存取其下的 Identity 物件的。

我們可以使用如下的程式將 ClaimsPrincipal.Claims 取出:

System.Security.Claims.ClaimsPrincipal principal = HttpContext.Current.User 
    as System.Security.Claims.ClaimsPrincipal;
if (null != principal)
{
    foreach (System.Security.Claims.Claim claim in principal.Claims)
    {
        lbIdentity.Text += string.Format(
            "Claim Type: <u>{0}</u>, Subject: <u>{1}</u>, Value: <u>{2}</u></br>",
            claim.Type, claim.Subject, claim.Value);
    }
}

其輸出結果如下:

Claim Type: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier, Subject: System.Security.Claims.ClaimsIdentity, Value: 161e163d-99f3-4f39-978c-51b28bc35846
Claim Type: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name, Subject: System.Security.Claims.ClaimsIdentity, Value: Johnny
Claim Type: http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider, Subject: System.Security.Claims.ClaimsIdentity, Value: ASP.NET Identity
Claim Type: http://schemas.microsoft.com/ws/2008/06/identity/claims/role, Subject: System.Security.Claims.ClaimsIdentity, Value: Manager
Claim Type: http://schemas.microsoft.com/ws/2008/06/identity/claims/role, Subject: System.Security.Claims.ClaimsIdentity, Value: Admin

從這裡我們應該可以看出其內部實作邏輯。

如果你對 Claims-Based Identity 的理論與實作有興趣的話, 可以參考這本免費的電子書: A Guide to Claims-Based Identity and Access Control, Second Edition

WIF 把帳戶和帳戶資料主要以兩個 interface 來定義:

public interface IUser
{
   string Id { get; }
   string UserName { get; set; }
}
 
public interface IUserStore<TUser> : IDisposable where TUser : IUser
{
   Task CreateAsync(TUser user);
   Task DeleteAsync(TUser user);
   Task<TUser> FindByIdAsync(string userId);
   Task<TUser> FindByNameAsync(string userName);
   Task UpdateAsync(TUser user);
}

在資料的部份, 它採用了 Entity Framework Code-First 技術做為它的 ORM (Object/Relational Mapping) 核心, 所以我們可以透過 EF 慣用的方式存取帳戶資料。不過, 很可惜的, 它在專案樣版中並沒有公開它的程式碼, 所以我們看不到它的資料定義方式。幸好, 我們還是可以從它建立的資料表看出其設計邏輯:

我們可以從 VS 的物件瀏覽器中看到相關類別的定義:

基本上, 這個 Identity 框架的最重要定義都可以在 Microsoft.AspNet.Identity.Core 和 Microsoft.AspNet.Identity.EntityFramework 這兩個組件裡找到。而如果你要操縱這個框架的話, 那麼 IdentityUser 和 UserManager 則是最重要的兩個類別 (或許還可以再加上一個 IdentityUserRole)。至於它的預設連線使用的是定義在 Web.config 中的 DefaultConnection 這個連線字串。

不過, 請稍為留意一下, 在專案範本中的 Model 下的 IdentityModels.cs 裡, 程式會自動加上一個空的 ApplicationUser 類別 (繼承 IdentityUser, 而 IdentityUser 又實作了 IUser), 以及一個空的 UserManager 類別 (繼承 UserManager<ApplicationUser>)。這兩個空的類別是為了讓你擴充你的客製化帳戶類別而設計的; 你不一定要使用這兩個名稱。詳細的做法可以參考「Customizing profile information in ASP.NET Identity in VS 2013 templates」一文。我個人採用的是文章中的第二種做法。

如何著手?

要建立一個使用表單認證的網站, 首先, 請先從 Web 範本下建立一個「ASP.NET Web 應用程式」專案 (請不要選到 Visual Studio 2012 目錄之下的範本)。然後, 在接下來的「選取範本」視窗中按下右側「變更驗證」按鈕, 選擇「個別使用者帳戶」(如果原來不是的話), 再按下「確定」按鈕以建立專案。

接著, 請將專案根目錄下的 Web.config 中的 DefaultConnection 這個 ConnctionString 改掉 (除非你想延用原來的 LocalDb 作為資料庫):

<add name="DefaultConnection" connectionstring="Data Source=MYPC\SQLEXPRESS;Initial Catalog=TestDb;Persist Security Info=True;User ID=sa;Password=^PassW0rcl$" providername="System.Data.SqlClient">

現在就可以按下 F5 以執行網站了。

如果你發現你的網站目前已登入, 請將它登出。然後按下右上角的「註冊」按鈕, 註冊一個新的帳戶。

現在你將發現你的資料庫中已自動建立好一個你在上述的 DefaultConnection 中指定的資料庫名稱的資料庫檔案, 其下有如同本文最上方圖片中列出的各個資料表。到這裡為止, 這個表單認證網站已經建立完成了。

存取帳戶資訊

如上所述, 建立網站很容易, 但是要如何存取帳戶資訊呢? 你可以使用至少兩種方式存取它的 Identity 資訊。

第一種, 就是透過 IdentityUser 類別, 以及 UserManager 這個 Helper 類別。例如你可以很輕鬆地透過 UserManager 取出對應帳戶的 Role 列表:

UserManager manager = new UserManager();
List<string> strRoles = manager.GetRoles(_Id).ToList();

第二種, 你可以在程式中直接取出資料庫裡的資料:

using (var db = new ApplicationDbContext())
using (var UserManager = new UserManager<applicationuser>(new UserStore<applicationuser>(db)))
{
    IdentityUser iUser = db.Users.Find(userId);
    List<identityuserrole> roles = iUser.Roles.ToList();
}

我想, 如果你是舉一能夠反三的開發者, 上面兩個簡單的程式應該就足夠你往下發展了。

此外, 你也應該參考一下範本中 Account 資料夾裡的幾個程式, 如此應該可以得到更多啟發。

如果要指定帳戶的 Role 的話, 也是使用相同的原則即可。只不過, 使用者界面就必須由你自己去設計了。

資料夾的權限設定

或許你想知道在 ASP.NET 中對資料夾的權限設定方式是不是跟以前一樣。是的, 和以前是差不多的。例如你有一個 Classified 資料夾, 那麼, 在這個資料夾裡加入一個如下的 Web.config 檔案:

<?xml version="1.0"?>
<configuration>

  <location path=".">
    <system.web>
      <authorization>
        <allow roles="Manager,Admin"/>
        <deny users="*"/>
      </authorization>
    </system.web>
  </location>

</configuration>

如此設定之後, 這個資料夾就只有符合 Manager 和 Admin 的角色可以存取了。

後記

或許 ASP.NET 的新的表單認證會讓許多開發者覺得緊張和不知所措, 但是一旦實際下手去做, 你就會發現它真的是異常的簡單。因此, 我這篇文章沒辦法寫得很長, 因為它確實不難。當然, 如果你要擴充它的功能, 你可能必須先去修習 Entity Framework (尤其是 Code-First) 技術, 但是嚴格說起來, 這部份的工夫並不能算在表單驗證上面。

最後再補充一點, 也許你會懷疑, 既然 Identity 框架已經使用了 Entity Framework Code-First, 那麼我們還能再加入另一套 Code-First 嗎? 答案是肯定的。我個人另外加入了幾個 Entity Framework Code-Fist 資料表 (即使同樣是透過 DefaultConnection), 跟既有的 Identity 框架不會衝突, 也可以透過 update-database 指令更新資料庫。所以同一個專案中並存兩個(或以上)的獨立的 DbContext 是完全可行的。

2 則留言: