之所以為上集,是因為我并沒有解決這個問題,寫這篇博文的目的是紀錄一下我所遇到的問題,以免自己忘記,其實已經(jīng)忘了差不多了,寫的過程也是自己回顧的過程,并且之前收集有關(guān) asp.net 5 身份驗證的書簽已經(jīng)太多了,所以必須記錄下來。
在前年(2014-12-10),我寫了這篇博文《愛與恨的抉擇:ASP.NET 5+EntityFramework 7》,背景是我當(dāng)時打算用 ASP.NET 5 重寫一個 Web 項目,因為那時候 ASP.NET 5 剛發(fā)布不久(之前叫 vNext),所以當(dāng)時抱了很大的激情投入在上面,但最后的結(jié)果是給自己澆了一盆冷水,放棄的原因文章中已經(jīng)總結(jié)了,關(guān)于為啥放棄 ASP.NET 5,就是因為身份驗證的問題,現(xiàn)在時間過去一年多了,現(xiàn)在回過頭來看,其實還是蠻有意思的,比如下面我說一個。
其實最后我想要的功能是不綁定 DbContext,在 ASP.NET 5 項目中,只進行判斷操作,身份驗證在另外服務(wù)中進行,然后在本項目中可以實現(xiàn)類似 FormsAuthentication.SetAuthCookie 操作就可以了,但最后做了幾個 Demo 都不能實現(xiàn),規(guī)定的一天時間,已經(jīng)用完了,所以。。。
上面我前年想要實現(xiàn)的想法,其實我現(xiàn)在也在做這個工作,但中間已經(jīng)過去一年多時間了,最后還是沒有實現(xiàn)。
登錄系統(tǒng)是一個獨立的站點,這是一個老的項目,身份驗證使用的是 Forms Authentication,因為涉及到其它站點,所以不能把登錄系統(tǒng)的身份驗證改寫為 Claims-based 或者 OAuth,這就意味著你需要讓其它站點的身份驗證方式,來兼容 Forms Authentication,登錄系統(tǒng)獨立的好處是,其它站點不需要管理用戶的登錄和注銷功能,只需要判斷用戶有沒有通過身份驗證即可,就像我當(dāng)時說的一樣,我只需要進行判斷操作,但最后做了很多 Demo 研究,還是實現(xiàn)不了,現(xiàn)在回過頭來看,當(dāng)時如果實現(xiàn)了才真是見鬼了,因為 ASP.NET 5 根本就不支持 Forms Authentication(后面詳細說),所以,懂得放棄也是好事,畢竟時間是寶貴的。
后來,那個 Web 項目放棄使用 ASP.NET 5 + EF 7,然后用 ASP.NET MVC 5 + EF 6 重寫完成了,但心里面還是很不甘心,其實在當(dāng)時我并不是很懂 ASP.NET Identity 身份驗證,所以也導(dǎo)致浪費了很多時間,后來花了點時間重新學(xué)習(xí)了 ASP.NET Identity,也就是記錄的這篇博文《跌倒了,再爬起來:ASP.NET 5 Identity》,這篇博文的主要內(nèi)容是查看 ASP.NET 5 Identity 的源碼,然后拋棄 applicationDbContext、UserManager、SignInManager 等等,直接實現(xiàn)用戶的登錄操作,并且成功實現(xiàn)驗證,看到博文最后,你會發(fā)現(xiàn) ASP.NET Identity 和之前的 Forms Authentication 還是有很多不同的,但都是基于 Cookie 加密的方式,下面看三段代碼:
Forms Authentication 方式登錄:
System.Web.Security.FormsAuthentication.SetAuthCookie("xishuai", false);
ASP.NET Identity 方式登錄(截止 2015-01-11):
var userId = await UserManager.GetUserIdAsync(user, cancellationToken);Context.Response.SignIn(StoreTwoFactorInfo(userId, loginPRovider));
ASP.NET Identity 方式登錄(最新,來自 SignInManager.cs):
var userId = await UserManager.GetUserIdAsync(user);await Context.Authentication.SignInAsync(Options.Cookies.TwoFactorUserIdCookieAuthenticationScheme, StoreTwoFactorInfo(userId, loginProvider));
首先,ASP.NET Identity 和 Forms Authentication 都是通過把用戶信息加密后,放入響應(yīng)頭的 Cookie 中,只不過兩種 Cookie 加密的方式不同(ASP.NET Identity 會更加復(fù)雜),所以如果登錄方式使用的 Forms Authentication,那在 ASP.NET 5 中就沒有辦法判斷用戶驗證,因為加密和解密要一一對應(yīng),如果不對應(yīng),那獲取到的 Cookie 就沒有辦法解密成功,所以也就沒有辦法通過身份驗證(IsAuthenticated 為 false),另外,關(guān)于 ASP.NET Identity,它不像一個技術(shù)點,有點類似于框架的概念,只不過把身份驗證的內(nèi)容包裝了一下,比如產(chǎn)生了 ApplicationDbContext、UserManager、SignInManager 等等,作用就是讓你使用更加方便,查看源碼就知道,其實核心內(nèi)容就是上面那些。
關(guān)于 SignInManager.cs 中的代碼,我們發(fā)現(xiàn)有很大的變化,比如 SignInAsync 中的代碼,Context.Authentication.SignInAsync
的實現(xiàn),我們可以從 Security 項目中找到,具體在 Microsoft.AspNet.Authentication/AuthenticationHandler.cs,感覺和之前的相比變的復(fù)雜了。
回到最初的問題:在 ASP.NET 5 中,如何實現(xiàn)身份驗證(兼容 Forms Authentication)?
上面的問題雖然看起來很簡單,但是有個首要前提:ASP.NET 5 不支持 Forms Authentication,那么這個問題就變得復(fù)雜了,但我們可以拆分下:
我們先研究第一問題,首先,我們不使用 ASP.NET 5 Identity,而是直接登錄進行身份驗證,為什么要這么做?因為登錄系統(tǒng)不能重寫,所以我們使用 ASP.NET 5 Identity 也沒有什么意義,況且多了一大堆不必要的東西(UserManager、SignInManager 等),會讓問題變的復(fù)雜,在之前的博文最后,有一個簡單示例,如下:
//app.UseIdentity();app.UseCookieAuthentication((cookieOptions) =>{ cookieOptions.AuthenticationType = IdentityOptions.ApplicationCookieAuthenticationType; cookieOptions.AuthenticationMode = AuthenticationMode.Active; cookieOptions.CookieHttpOnly = true; cookieOptions.CookieName = ".CookieName"; cookieOptions.LoginPath = new PathString("/Account/Login"); cookieOptions.CookieDomain = ".mysite.com";}, "AccountAuthorize");[AllowAnonymous]public IActionResult Login(string returnUrl = null){ var userId = "xishuai"; var identity = new ClaimsIdentity(IdentityOptions.ApplicationCookieAuthenticationType); identity.AddClaim(new Claim(ClaimTypes.Name, userId)); Response.SignIn(identity); return Redirect(returnUrl);}
上面是一年前的代碼,一年后變成了這樣:
//app.UseIdentity();app.UseCookieAuthentication((cookieOptions) =>{ cookieOptions.AutomaticAuthenticate = true; cookieOptions.AutomaticChallenge = true; cookieOptions.CookieHttpOnly = true; cookieOptions.ExpireTimeSpan = TimeSpan.FromMinutes(43200); cookieOptions.LoginPath = new PathString("/account/login"); cookieOptions.CookieName = ".CNBlogsCookie"; cookieOptions.CookiePath = "/";});public async Task<IActionResult> Login(string returnUrl = null){ var userId = "xishuai"; var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); identity.AddClaim(new Claim(ClaimTypes.Name, userId)); await HttpContext.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), new AuthenticationProperties() { IsPersistent = true }); return Redirect(returnUrl);}
上面看似沒問題的代碼,但實際使用中遇到了很多的問題,比如生成 Cookie 的 Expires 為 session,也就是我們設(shè)置的 ExpireTimeSpan 沒有作用,解決方式:SignInAsync 需要傳遞一個 new AuthenticationProperties() { IsPersistent = true }
參數(shù),另外還有其它問題,我現(xiàn)在已經(jīng)記不得了,不過記錄了一個 Issue:HttpContext.Authentication.SignInAsync not working,再貼一下 project.json 中程序包版本,后來測試很多次,可能是版本不一致引起的:
"dependencies": { "Microsoft.AspNet.Authentication.Cookies": "1.0.0-rc2-16160", "Microsoft.AspNet.DataProtection.Extensions": "1.0.0-rc2-15874", "Microsoft.AspNet.Diagnostics": "1.0.0-rc2-16303", "Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc2-15994", "Microsoft.AspNet.Mvc": "6.0.0-rc2-16614", "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc2-16614", "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc2-16156", "Microsoft.AspNet.StaticFiles": "1.0.0-rc2-16036", "Microsoft.AspNet.Tooling.Razor": "1.0.0-rc2-15994", "Microsoft.Extensions.Configuration.FileProviderExtensions": "1.0.0-rc2-15905", "Microsoft.Extensions.Configuration.Json": "1.0.0-rc2-15905", "Microsoft.Extensions.Logging": "1.0.0-rc2-15907", "Microsoft.Extensions.Logging.Console": "1.0.0-rc2-15907", "Microsoft.Extensions.Logging.Debug": "1.0.0-rc2-15907", "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-rc2-16142"}
后來折騰了很久,測試可以使用了,但發(fā)布到服務(wù)器的時候,又出現(xiàn)了問題,因為站點使用的是負載均衡,需要把程序發(fā)布到兩臺服務(wù)器上,當(dāng)兩臺服務(wù)器同時在跑的時候,比如登錄請求到一臺服務(wù)器,驗證剛好請求到另一臺服務(wù)器,這時候身份驗證就沒有效果,然后跳轉(zhuǎn)到登錄頁面,這個問題折騰我很久,自己怎么配置都不行,后來沒有辦法,向微軟提了一個 Issue:Multiple web servers CookieAuthentication does not work,問題提出后,很快有人回復(fù)了,問題原因是需要提供一個 key,這個有點像 Forms Authentication 方式中 Web.config 的 MachineKey,我們需要將身份驗證的配置,修改如下:
var dataProtection = new Microsoft.AspNet.DataProtection.DataProtectionProvider(new DirectoryInfo(@"c:/shared-auth-ticket-keys/"));app.UseCookieAuthentication((cookieOptions) =>{ cookieOptions.AutomaticAuthenticate = true; cookieOptions.AutomaticChallenge = true; cookieOptions.CookieHttpOnly = true; cookieOptions.ExpireTimeSpan = TimeSpan.FromMinutes(43200); cookieOptions.LoginPath = new PathString("/account/login"); cookieOptions.CookieName = ".CNBlogsCookie"; cookieOptions.CookiePath = "/"; cookieOptions.DataProtectionProvider = dataProtection;});
后來重新發(fā)布,測試還是出現(xiàn)問題,和之前的問題一樣,跳轉(zhuǎn)到登錄頁面,然后我嘗試把一臺服務(wù)器生成在 c:/shared-auth-ticket-keys
目錄下的 key 文件,拷貝到另外一臺服務(wù)器中,但還是沒用,過了很多天,有人回復(fù)了:
You need to point the key directory to a shared directory which both applications can access. Putting it in c:/shared-auth-ticket-keys/ isn't enough in multiple server scenarios, as it's still going to create a key ring local to each machine.
You need to create an UNC share somewhere that both applications can access, and use that, for example /keystore/keystore
Or you implement a key store yourself suitable to your architecture, for example, using SQL Server.
大致意思是,雖然是同一個目錄,但會在不同服務(wù)器生成不同的 key 文件,所以身份驗證就不通過,解決方式是使用 key 共享文件,這樣讓不同服務(wù)器都能訪問同一個 key 文件,另外一種方式是將 key 存儲在一個地方,比如 SQL Server 中,但我不是很了解 key 的讀取和存儲方式,所以,我最后嘗試用第一種方式解決,只需要我們將目錄更改為共享目錄:
var dataProtection = new Microsoft.AspNet.DataProtection.DataProtectionProvider(new DirectoryInfo(@"//10.10.10.10/shared-auth-ticket-keys/"));
后來再重新發(fā)布,還是出現(xiàn)了問題,比如共享文件放在一臺服務(wù)器上,這臺服務(wù)器訪問沒用什么問題,但另一臺服務(wù)器卻不能訪問,文件資源管理器可以訪問此共享文件,這個問題也折騰我很久,但不和 ASP.NET 5 相關(guān),主要問題是不了解 ASP.NET 如何訪問共享文件,后來找資料解決了,記錄了一篇博文:ASP.NET 訪問共享文件夾。
目前的情況:第一個問題已經(jīng)實現(xiàn),但是比較簡陋,開始考慮并實現(xiàn)第二個問題。
一開始的時候,我提了一個 Issue:Share ASP.NET MVC 5 Forms authentication?
這個 Issue 我覺得很有價值,它讓我了解了很多東西,比如 ASP.NET 5 不支持 Forms Authentication,ASP.NET 5 和 Forms Authentication 的 Cookie 加密方式不同,ASP.NET 5 會更加復(fù)雜,因為登錄系統(tǒng)不能被重寫,并且 ASP.NET 5 不支持 Forms Authentication,那么擺在我面前的只有一條路,在 ASP.NET 5 中,解密 Cookie(通過 Forms Authentication 加密),針對這個問題,我的一些想法:
其實看起來這個問題好像不是很復(fù)雜,通過 Key 加密生成 Cookie(Forms Authentication),然后通過下面方式獲取 Cookie(ASP.NET 5):
var cookies = Request.Cookies.First(x => x.Key == ".CNBlogsCookie").Value;
然后通過某些手段解密生成 IdentityUser 對象,對,沒錯,就這么簡單。
我們先不住 ASP.NET 5 中實現(xiàn)下,很簡單:
var cookies = "";FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(cookies);string[] roles = authTicket.UserData.Split(new char[] { ';' });var user = new GenericPrincipal(User.Identity, roles);
這段代碼是執(zhí)行成功的,但我們需要在 Web.config 中,配置如下代碼:
這段代碼必須要和登錄站點中的配置一樣,原因是加密和解密的方式要一一對應(yīng),接下來的工作,我們需要在 ASP.NET 5 中實現(xiàn)上面的代碼,但你會發(fā)現(xiàn)找不到 FormsAuthentication.Decrypt
了,這么辦呢?只能查看源碼,然后把相關(guān)代碼貼出來編譯一下,如果成功了(我嘗試了很多次,因為涉及的代碼太多,實現(xiàn)起來非常困難),這是第一步,第二步我們將編譯通過的代碼,放在 ASP.NET 5 中再編譯一次,這個工作我還沒做,不過看起來并不是那么簡單,因為運行時和基礎(chǔ)類庫都發(fā)生變化了。
如果重寫這部分代碼,我貼一下需要的一些資源(后面再嘗試下):
后來,上面那個 Issue 有人回復(fù)如下:
看到這,有點想哭的趕腳,但不管怎樣,還是要嘗試下,希望下集是一個成功的博文記錄,未完待續(xù)。。。
最后,貼一下這段時間累積的有關(guān)資料:
新聞熱點
疑難解答