在Membership系列的最后一篇引入了ASP.NET Identity,看到大家對它還是挺感興趣的,于是來一篇詳解登錄原理的文章。本文會涉及到Claims-based(基于聲明)的認證,我們會詳細介紹什么是Claims-based認證,它與傳統認證方式的區別,以及它的特點。同時我們還會介紹OWIN (Open Web Interface for .NET) 它主要定義了Web Server 和Web Application之間的一些行為,然后實現這兩個組件的解耦(當然遠不止這么點東西,我相信OWIN馬上就會掀起一場血雨腥風)ASP.NET Identity是如何利用OWin實現登錄的,都是干貨,同學,你準備好學習了么?
目錄
ASP.NET Identity登錄原理
廢話少說,我們直接切入正題。在上一篇從Membership到ASP.NET Identity,我們已經給了一個簡單的實例,并且大致的描述了一下ASP.NET Identity的結構體系,但是ASP.NET Identity主要提供的功能是幫助我們管理用戶,角色等信息,它主要負責的是存儲這一塊,也就是我們的信息存到哪里去的問題。但是用戶是如何實現登錄的? 是Forms認證么?用到Cookie了么? Cookie里面有保存明文信息么(咳咳,最近某程旅游網好像很火?),接下來我們就來一一的回答這些問題。
在上一篇的例子中,我們可以簡單的發現,要實現登錄實際上只有簡單的三行代碼
private IAuthenticationManager AuthenticationManager { get { return HttpContext.GetOwinContext().Authentication; } } private async Task SignInAsync() { // 1. 利用ASP.NET Identity獲取用戶對象 var user = await UserManager.FindAsync("UserName", "Password"); // 2. 利用ASP.NET Identity獲取identity 對象 var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); // 3. 將上面拿到的identity對象登錄 AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = true }, identity); }
我們發現UserManager.CreateIdentityAsync返回給我們的對象是一個ClaimsIdentity,這又是一個什么玩意?它和我們原來所熟知的Identity對象有什么關聯么?畢竟長的那么像,在深入之前,我們先要了解一下下面的概念。
什么是Claims-based(基于聲明)的認證
首先這個玩意不是微軟特有的,Claims-based認證和授權在國外被廣泛使用,包括微軟的ADFS,Google,Facebook等。 國內我就不知道了,沒有使用過國內的第三方登錄,有集成過QQ登錄或者支付寶登錄的同學可以解釋一下。
Claims-based認證主要解決的問題?
對比我們傳統的Windows認證和Forms認證,claims-based認證這種方式將認證和授權與登錄代碼分開,將認證和授權拆分成另外的web服務。活生生的例子就是我們的qq集成登錄,未必qq集成登錄采用的是claims-based認證這種模式,但是這種場景,千真萬確就非常適合claims-based認證。
Claims-based認證的主要特點:
- 將認證與授權拆分成獨立的服務
- 服務調用者(一般是網站),不需要關注你如何去認證,你用Windows認證也好,用令牌手機短信也好,與我無關。
- 如果用戶成功登錄的話,認證服務(假如是QQ) 會返回給我們一個令牌。
- 令牌當中包含了服務調用者所需要的信息,用戶名,以及角色信息等等。
總的來說就是,我再也不用管你怎么登錄,怎么樣去拿你有哪些角色了,我只需要把你跳到那個登錄站點上,然后它返回給我令牌信息,我從令牌上獲取需要的信息來確定你是誰,你擁有什么角色就可以了。
進一步理解Claims-based 認證
為了讓大家進一步理解Claims-based認證,我們從一個普通的登錄場景開始說起,拿QQ集成登錄來舉例。
- 用戶跑到我們的網站來訪問一個需要登錄的頁面
- 我們的網站檢測到用戶沒有登錄,返回一個跳轉到QQ登錄頁的響應(302 指向QQ登錄頁面的地址并加上一個返回的鏈接頁面,通常是returnUrl=)
- 用戶被跳轉到指定QQ的登錄頁面
- 用戶在QQ登錄頁面上輸入用戶名和密碼,QQ會到自己的數據庫中查詢,一旦登錄成功,會返回一個跳轉到我們站點的響應(302指向我們的網站頁面)
- 用戶被跳轉到我們網站的一個檢測登錄的頁面,我們可以拿到用戶的身份信息,建立ClaimsPrinpical和ClaimsIdentity對象,生成cookie等。
- 我們再把用戶帶到指定的頁面,也就是returnUrl,那是用戶登錄前最后一次訪問的頁面
簡單的來說,就是把登錄的代碼(驗證用戶,獲取用戶信息)拆分成獨立的服務或組件。
ASP.NET 下的 Claims-based認證實現
說完什么是Claims-based認證之后,我們接下來就可以看看ClaimsIdentity以及ClaimsPrincipal這兩個類,他們是.NET下Claims-based認證的主要基石。當然正如我們所想,他們繼承了接口IIdentity和IPrincipal。
IIdentity封裝用戶信息
這個接口很簡單,它只包含了三個最基本的用戶身份信息。
IPrincipal 代表著一個安全上下文
這個安全上下文對象包含了上面的identity以及一些角色和組的信息,每一個線程都會關聯一個Principal的對象,但是這個對象是屬性進程或者AppDomain級別的。ASP.NET自帶的 RoleProvider就是基于這個對象來實現的。
CalimsIdentity和ClaimsPrincipal
在System.Security.Claims命名空間下去,我們可以發現這兩個對象。下面我們來做一個小例子,這個小例子會告訴我們這兩個對象是如何進行認證和授權的。我們要做的demo很簡單,建一個空的mvc站點,然后加上一個HomeController,和兩個Action。并且給這個HomeController打上Authroize的標簽,但是注意我們沒有任何登錄的代碼,只有這個什么也沒有的Controller和兩個什么也沒有的Action。
當然,結果也是可想而知的,我們得到了401頁面,因為我們沒有登錄。
下面我們就來實現登錄,這里的登錄非常簡單,我們手動去創建這個ClaimsIdentity和ClaimsPrincipal對象,然后將Principal對象指給當前的HttpContext.Current.User。
我們在Global.asax中添加了Application_AuthenticateRequest方法,也就是每次MVC要對用戶進行認證的時候都會進到我們這個方法里面,然后我們就這樣神奇的把用戶給登錄了。
當然,我們沒有Home/Manager的訪問權限,因為我們上面只給了用戶Users的Role。
現在大家知道ClaimsIdentity和ClaimsPrincipal是如何使用了么?這里要注意一下的是,我們沒有設置IsAutheiticated為true,在.NET4.5以前,對于GenericIdentity只要設置它的Name的時候IsAutheiticated就自動設置為true了,而對于ClaimsIdentity是在它有了第一個Claim的時候。在.NET4.5以后,我們就可以靈活控制了,默認ClaimsIdentity的IsAutheiticated是false,只有當我們構造函數中指定Authentication Type,它才為true。
最后結論,我們講了ClaimsIdentity什么的,講了這么多和今天的主題有嘛關系?我們上面說ASPNET Identity登錄有三句話,第一句話可以略過,第二句話就是我們上面講的。
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
UserManager實際上只是為我們創建了一個ClaimsIdentity的對象,還是通過我們自己從數據庫里面取出來的對象來創建的,它也就干了那么點事,一層小小的封裝而已。不要被后面的DefaultAuthenticationTypes.ApplicationCookie嚇到了,這里還沒有和cookie扯上半點關系,這就是一個字符串常量,和我們上面自己定義的MyClaimsLogin是沒有區別的。
到這里,我想算是把登錄代碼的第二句話講完了,講清楚了,那么我們來看看第三句話,也就是最后一句,其實它才是登錄的核心,第二句只是創建了一個ClaimsIdentity的對象。
private IAuthenticationManager AuthenticationManager { get { return HttpContext.GetOwinContext().Authentication; } } AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = true }, identity);
通過F12查看,發現IAuthenticationManager 在 Microsoft.Owin.Security命名空間下,而這個接口是定義在Microsoft.OWin.dll中的。這又是個什么玩意兒?帶著這個疑問,我開始了我的OWin學習之旅。
到底什么是OWIN
首先我們來簡單介紹一下OWin,它是由微軟ASP.NET小組成員組織成立的一個開源項目。目標是解耦服務器和應用,這里面的服務器主要是指web 服務器,比如說IIS等,全稱是Open Web Interface for .Net。OWin可以說是一套定義,默認它是沒有什么具體的實現的,那么在它的定義里面是如何實現服務器與應用程序的解耦的呢? 我們又該如何理解服務器與應用程序的解耦呢?
下面是個人的理解,拋磚引玉,希望大家多探討。
問題引入: 為什么要解耦服務器與應用程序 ?
既然是服務器和應用程序的解耦,那么這肯定是我們第一個應該考慮的問題。我們先來簡單復習一下ASP.NET 或者是IIS 集成模式管道模型,也就是說一個http請求在進入IIS之后 (我們這里指7.0及以后版本的集成模式),一直到返回response這中間所經歷的步驟。
大家知道,我們可以開發自己的Http Module去注冊這些事件,然后做相應的處理。比如說FormsAuthenticationModule就是注冊了AuthenticateRequest事件,然后在這里面去檢查用戶的cookie信息來判斷用戶是否登錄的,這里就是一個典型的應用程序與服務器之間的交互問題。而這些事件最后是被IIS觸發的,我們是通過web.config把我們自定義的http module注冊進了iis。回到我們的問題,如果我們的網站不運行在iis了,我們自己開發的這些Http module還能使用么?
另外的問題就是,大家知道我們在ASP.NET 里面經常用到HttpContext,HttpApplicationt等對象,而ASP.NET所有的處理基本上都離不開這兩個對象,因為我們的Request以及Response都是封裝在HttpContext里面的,而這些信息是從IIS中來,最后也是交給IIS處理,因為微軟給IIS寫代碼的時候直接集成了這一塊,但是想一下,如果web服務器不是IIS,那么這些信息又從哪里獲取呢?
為什么需要解耦,是因為他們彼此之間的依懶過大,從而導致我們不能夠輕易的換掉其中任何一個。 即使現在,在web.config添加自己定義的http module 也不是一件能讓人開心的事情,反正我一想到那個很長的類名以及程序集名就夠蛋疼的。
顯然,很多人已經開始意識到,在如今web飛快發展的年代,這種模式已經不能夠滿足靈活多變的需求。越是輕量級,組件化的東西,越能夠快速適應變化,更何況.NET現在要吸引開源社區的注意,只有把這一塊打通了,越來越多的強大的開源組件才能夠出現在.NET的世界里,比如說寫一個開源的ASP.NET web服務器。
OWin如何做到解耦
我們上面說Owin是一套定義,它通過將服務器與應用程序之間的交互歸納為一個方法簽名,稱之為“應用程序代理(application delegate)”
AppFunc = Func<IDictionary<string, object>, Task>;
在一個基于Owin的應用程序中的每一個組件都可以通過這樣的一個代理來與服務器進行交互。 這們這里的交互其實是與服務器一起來處理http request,比如說ASP.NET管理模型中的那些事件,認證,授權,緩存等等,原先我們是通過自定義的http module,在里面拿到包含了request和response的HttpContext對象,進行處理。而現在我們能拿到的就是一個Dictionary。
可是別小看了這個Dictionary,我們所有的信息比如Application state, request state,server state等等這些信息全部存在這個數據結構中。這個dictionary會在Owin處理request的管道中進行傳遞,沒錯有了OWin之后,我們就不再是與ASP.NET 管道打交道了,而是OWin的管道,但是這個管道相對于ASP.NET 管道而言更靈活,更開放。
這個字典在OWin管道的各個組件中傳輸時,你可以任意的往里面添加或更改數據。 OWin默認為我們定義了以下的數據:
有了這些數據以后,我們就不需要和.NET的那些對象打交道了,比如說ASP.NET MVC中的HttpContextBase, 以及WEB API 中的HttpRequestMessage和HttpResponseMessage。我們也不需要再考慮system.web 這個dll里的東西,我們只需要通過OWin就可以拿到我們想要的信息,做我們想做的事了。而OWin,它本身和web服務器或者IIS沒有任何關系。
微軟對OWin的開源實現Katana
我們上面講到了OWin只是一套定義,它本身沒有任何代碼,我們可以把它看成是微軟對外公開的一套標準。那么我們用到的Microsoft.OWin,這些dll又是從哪里來的呢? 好消息是它是開源的,代碼我們可以從CodePlex上下載,壞消息是它現在還沒有比較全的文檔,可能是我暫時還沒有找到。
它包括下面4個組件:
- Host: 托管我們應用程序的進程,或者宿主,可以是IIS,可以我們自己寫的程序等。主要是用來啟動,加載OWin組件,以及合理的關閉他們
- Server: 這個Server就是用來暴露TCP端口,維護我們上面講到的那個字典數據,然后通過OWin管理處理http請求
- Middleware : 這個中間件就是用來在OWin管道中處理請求的組件,你可以把它想象成一個自定義的httpModule,它會被注冊到OWin管道中一起處理http request
- Application: 這個最好理解,就我們自己開發的那個應用程序或者說是網站
也就是說我們用到的Microsoft.OWin,那一系列的dll實現上是叫Katana(武士刀)象征著快,狠,準!下面來一些名詞解釋,是一些簡單的概念有助于大家理解我們下面要講的內容(ASP.NET Identity是如何借助 OWin來實現登錄的)。
OWin Application( OWin 應用程序 )
這個程序引入了OWin的dll,同時會使用OWin中的一些組件完成對request的一些處理,比如說我們下面要講的OWin 認證。
OWin 組件
我們也可能管它叫中間件,它通過暴露一個應用程序代理,也就是接收一個IDictionary<string,object>,返回一個Task來參與到OWin對request和處理管道中。
每一個OWin的應用程序都需要有一個start up的類,用來聲明我們要使用的OWin組件(即中間件)。Start up 類有以下幾種聲明方式:
- 命名約定: Owin會掃描在程序集的根下名叫 startup的類作為默認啟動配置類
- OwinStartup 標簽
[assembly: OwinStartup(typeof(StartupDemo.TestStartup))]
- config 文件
<add key="owin:AutomaticAppStartup " value="false" />
OWin authentication
Owin的很大亮點之一就是它可以讓我們的ASP.NET 網站擺脫IIS,但是畢竟大多數的ASP.NET 網站還是host在IIS上的,所以Katana項目還支持在IIS集成模式中運行Owin組件。 我們只需要在我們的項目中加上Microsoft.Owin.Host.SystemWeb這個包就可以了,其實默認MVC5程序已經為我們加上了。我們在VS2013中新建一個MVC5的站點,默認會為我們加上以下的dll:
- OWin.dll
- Microsoft.Owin.dll
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security
- Microsoft.Owin.Security.Cookie
他們對應nuget中的package:
這就是為什么我們可以拿到Microsoft.Owin.Security.IAuthenticationManager,然后再調用其 SignIn方法和SignOut方法。除了多了這些dll以外,VS還自動幫我們移除了FormsAuthenticationModule。
Forms 認證
我們來小小的復雜一下Forms認證,在Forms認證中我們檢測完用戶名和密碼之后,只需要調用下面的代碼就會為我們創建用戶cookie。
FormsAuthentication.SetAuthCookie("Jesse", false);
然后FormsAuthenticateionModule會在ASP.NET 管道的 AuthenticateRequest 階段去檢查是否有這個cookie,并把它轉換成我們需要的identity對象,這樣的話我們就不需要每一次都讓用戶去輸入用戶名和密碼了。所以登錄的過程實現上是這樣的。
- 用戶在沒有登錄的情況下訪問了我們需要登錄的頁面
- FormsAuthenticationModule檢查不到用戶身份的cookie,沒有生成identity對象,HttpContext.User.IsAuthenticated = false
- 在ASP.NET 管道 的Authroize 授權階段,將用戶跳轉到登錄頁面
- 用戶輸入用戶名和密碼點擊提交
- 我們檢查用戶名和密碼,如果正確,就調用FormsAuthentication.SetAuthCookie方法生成登錄cookie
- 用戶可以正常訪問我們需要登錄的頁面了
- 用戶再次訪問我們需要登錄的頁面
- FormsAuthenticationModule檢查到了用戶身份的cookie,并生成identity對象,HttpContext.User.IsAuthenticated = true
- ASP.NET 管道的 Authroize授權階段,HttpContext.User.IsAuthenticated=true,可以正常瀏覽
- 7,8,9 循環
Forms認證有以下幾不足:
- 用戶名直接暴露在cookie中,需要額外的手段去將cookie加密
- 不支持claims-based 認證
- ....
我們上面Forms的登錄過程,對于OWin登錄來說同樣適用。我們在上面講ASP.NET Identity登錄第二句話的時候已經拿到了ClaimsIdentity,那么我們接下來要看的問題就是如何借助于IAuthenticationManager 去登錄? FormsAuthenticationModuel沒有了,誰來負責檢測cookie?您請接著往下看!
MVC 5默認的start up配置類
VS除了為我們引用OWin相關dll,以及移除FormsAuthenticationModule以外,還為我們在App_Start文件夾里添加了一個Startup.Auth.cs的文件。
public partial class Startup { public void ConfigureAuth(IAppBuilder app) { // 配置Middleware 組件 app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login"), CookieSecure = CookieSecureOption.Never, }); } }
UseCookieAuthentication是一IAppBuilder 的一個擴展方法,定義在Microsoft.Owin.Security.Cookies.dll中。
CookieAuthenticationExtensions.cs
public static IAppBuilder UseCookieAuthentication(this IAppBuilder app, CookieAuthenticationOptions options) { if (app == null) { throw new ArgumentNullException("app"); } app.Use(typeof(CookieAuthenticationMiddleware), app, options); app.UseStageMarker(PipelineStage.Authenticate); return app; }
將Owin Middleware綁定到IIS 集成模式的管道
UseCookieAuthentication主要調用了兩個方法:
- IAppBuilder.Use : 添加OWin middleware 組件到 OWin 管道
- IAppBuilder.UseStageMarker : 為前面添加的middleware指定在IIS 管道的哪個階段執行。
PepelineStage這個枚舉定義和我們IIS管道的那些順序,也就是和我們Http Module里面可以綁定的那些事件是一樣的。
我們可以回顧一樣如何在http module中為Authenticate綁定事件。
public class MyModule : IHttpModule { public void Init(HttpApplication context) { // 將ctx_AuthRequest 綁定的 AuthenticateRequest 事件 context.AuthenticateRequest += ctx_AuthRequest; } void ctx_AuthRequest(object sender, EventArgs e) { } }
Owin這里的Use,貌似是借用了Node.js的用法呀- -! 不管怎么說,通過這樣一種方式,我們就可以將Owin 中間件注冊進IIS 集成模式的管道了。也就是說我們上面注冊的CookieAuthenticationMiddleware會在AuthenticaRequest 階段執行。而它就是真正生成cookie以及讀取cookie的那只背后的手。
CookieAuthenticationMiddelware 負責讀取用戶信息cookie
如果你看的還算認真的話,我們上面講claims-based認證的時候有一個小例子。我們只需要在AuthenticateRequest階段將ClaimsPrincipal賦給當前的User對象就可以實現登錄了,而這也是IAuthenticationManager.SignIn所做的。但是我們上面講Forms登錄的過程一樣,用戶登錄之后,我們需要生成cookie,這樣用戶下次訪問的時候就不需要登錄了,我們在Authenticate Request去檢測有沒有這個cookie就可以了,CookieAuthenticationMiddleware就負責做了這兩件事情。
我們可以到Katana的站點去下載源碼,然后找到CookieAuthenticationMiddleware這個類,然后找到最后生成cookie和讀取cookie的類:CookieAuthenticationHandler。
在CookieAuthenticationMiddleware中有兩個方法:
- AuthenticateCoreAsync : 從request中讀取cookie值,附給到identity對象,沒有什么內幕,就是讀讀cookie進行解密,轉成identity對象。
- ApplyResponseGrantAsync : 往response中寫入cookie值,同樣沒有什么內幕,有興趣的同學可以下載katana源碼瞅瞅。
CookieAuthenticationMiddelware 對cookie的加密方式
在我們上篇文章中對ASP.NET Identity登錄的例子中,如果你登錄了,那么你會發現我們的cookie是經過加密的。而cookie的名稱是以.AspNet.為前綴加上我們
Startup中配置的AuthenticationType。
那么接下來,我們就來看一下CookieAuthenticationMiddleware是以什么樣的加密方式將我們的identity信息加密的,我們能不能將它解回來呢?
欲知后事如何,請聽下回分解~
參考&小結
這一篇文章涉及到的新東西比較多,也花了我不少的時間。但是總的來說收獲還是蠻大的,把Claims-based總結性的概括了一下,然后又開始了Owin的學習之旅。越來越發現.NET的強大,在開源社區的不斷貢獻下.NET也逐漸開始綻放出新的生命力。后面還會繼續Owin的學習,有興趣的朋友可以繼續關注!還是我一直強調的,雖然ASP.NET Identity登錄只有三行代碼,但是背后卻隱藏的如此之深,如果你不懷著一顆好奇以及好學的心,你永遠不知道背后有多么美麗的故事。如果只是習慣了使用框架,而不去了解它,那就會迷失在學習新技術的路上。但是如果你知道它的設計原理,你就會發現其實都差不多的,思想就是那么一套,但是外觀卻可能隨時改變。 另外的話我覺得這篇文章寫的不錯,值得點贊,你覺得呢?
我是Jesse Liu,關注我,跟我一起探尋.NET 那些新鮮,好玩,以及背后的故事吧 :)
參考資料:
- http://owin.org/
- http://brockallen.com/2013/10/24/a-primer-on-owin-cookie-authentication-middleware-for-the-asp-net-developer/
- http://www.asp.net/aspnet/overview/owin-and-katana/an-overview-of-project-katana
- http://www.asp.net/aspnet/overview/owin-and-katana/owin-middleware-in-the-iis-integrated-pipeline
- http://www.asp.net/aspnet/overview/owin-and-katana/owin-startup-class-detection
- http://msdn.microsoft.com/en-us/library/ff359101.aspx
文章列表