文章出處

之所以為上集,是因為我并沒有解決這個問題,寫這篇博文的目的是紀錄一下我所遇到的問題,以免自己忘記,其實已經忘了差不多了,寫的過程也是自己回顧的過程,并且之前收集有關 ASP.NET 5 身份驗證的書簽已經太多了,所以必須記錄下來。

在前年(2014-12-10),我寫了這篇博文《愛與恨的抉擇:ASP.NET 5+EntityFramework 7》,背景是我當時打算用 ASP.NET 5 重寫一個 Web 項目,因為那時候 ASP.NET 5 剛發布不久(之前叫 vNext),所以當時抱了很大的激情投入在上面,但最后的結果是給自己澆了一盆冷水,放棄的原因文章中已經總結了,關于為啥放棄 ASP.NET 5,就是因為身份驗證的問題,現在時間過去一年多了,現在回過頭來看,其實還是蠻有意思的,比如下面我說一個。

其實最后我想要的功能是不綁定 DbContext,在 ASP.NET 5 項目中,只進行判斷操作,身份驗證在另外服務中進行,然后在本項目中可以實現類似 FormsAuthentication.SetAuthCookie 操作就可以了,但最后做了幾個 Demo 都不能實現,規定的一天時間,已經用完了,所以。。。

上面我前年想要實現的想法,其實我現在也在做這個工作,但中間已經過去一年多時間了,最后還是沒有實現。

登錄系統是一個獨立的站點,這是一個老的項目,身份驗證使用的是 Forms Authentication,因為涉及到其它站點,所以不能把登錄系統的身份驗證改寫為 Claims-based 或者 OAuth,這就意味著你需要讓其它站點的身份驗證方式,來兼容 Forms Authentication,登錄系統獨立的好處是,其它站點不需要管理用戶的登錄和注銷功能,只需要判斷用戶有沒有通過身份驗證即可,就像我當時說的一樣,我只需要進行判斷操作,但最后做了很多 Demo 研究,還是實現不了,現在回過頭來看,當時如果實現了才真是見鬼了,因為 ASP.NET 5 根本就不支持 Forms Authentication(后面詳細說),所以,懂得放棄也是好事,畢竟時間是寶貴的。

后來,那個 Web 項目放棄使用 ASP.NET 5 + EF 7,然后用 ASP.NET MVC 5 + EF 6 重寫完成了,但心里面還是很不甘心,其實在當時我并不是很懂 ASP.NET Identity 身份驗證,所以也導致浪費了很多時間,后來花了點時間重新學習了 ASP.NET Identity,也就是記錄的這篇博文《跌倒了,再爬起來:ASP.NET 5 Identity》,這篇博文的主要內容是查看 ASP.NET 5 Identity 的源碼,然后拋棄 ApplicationDbContext、UserManager、SignInManager 等等,直接實現用戶的登錄操作,并且成功實現驗證,看到博文最后,你會發現 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 都是通過把用戶信息加密后,放入響應頭的 Cookie 中,只不過兩種 Cookie 加密的方式不同(ASP.NET Identity 會更加復雜),所以如果登錄方式使用的 Forms Authentication,那在 ASP.NET 5 中就沒有辦法判斷用戶驗證,因為加密和解密要一一對應,如果不對應,那獲取到的 Cookie 就沒有辦法解密成功,所以也就沒有辦法通過身份驗證(IsAuthenticated 為 false),另外,關于 ASP.NET Identity,它不像一個技術點,有點類似于框架的概念,只不過把身份驗證的內容包裝了一下,比如產生了 ApplicationDbContext、UserManager、SignInManager 等等,作用就是讓你使用更加方便,查看源碼就知道,其實核心內容就是上面那些。

關于 SignInManager.cs 中的代碼,我們發現有很大的變化,比如 SignInAsync 中的代碼,Context.Authentication.SignInAsync 的實現,我們可以從 Security 項目中找到,具體在 Microsoft.AspNet.Authentication/AuthenticationHandler.cs,感覺和之前的相比變的復雜了。

回到最初的問題:在 ASP.NET 5 中,如何實現身份驗證(兼容 Forms Authentication)?

上面的問題雖然看起來很簡單,但是有個首要前提:ASP.NET 5 不支持 Forms Authentication,那么這個問題就變得復雜了,但我們可以拆分下:

  1. 了解現階段 ASP.NET 5 身份驗證的實現方式。
  2. 在 ASP.NET 5 中,解密 Cookie(通過 Forms Authentication 加密)。

我們先研究第一問題,首先,我們不使用 ASP.NET 5 Identity,而是直接登錄進行身份驗證,為什么要這么做?因為登錄系統不能重寫,所以我們使用 ASP.NET 5 Identity 也沒有什么意義,況且多了一大堆不必要的東西(UserManager、SignInManager 等),會讓問題變的復雜,在之前的博文最后,有一個簡單示例,如下:

//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,也就是我們設置的 ExpireTimeSpan 沒有作用,解決方式:SignInAsync 需要傳遞一個 new AuthenticationProperties() { IsPersistent = true } 參數,另外還有其它問題,我現在已經記不得了,不過記錄了一個 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"
}

后來折騰了很久,測試可以使用了,但發布到服務器的時候,又出現了問題,因為站點使用的是負載均衡,需要把程序發布到兩臺服務器上,當兩臺服務器同時在跑的時候,比如登錄請求到一臺服務器,驗證剛好請求到另一臺服務器,這時候身份驗證就沒有效果,然后跳轉到登錄頁面,這個問題折騰我很久,自己怎么配置都不行,后來沒有辦法,向微軟提了一個 Issue:Multiple web servers CookieAuthentication does not work,問題提出后,很快有人回復了,問題原因是需要提供一個 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;
});

后來重新發布,測試還是出現問題,和之前的問題一樣,跳轉到登錄頁面,然后我嘗試把一臺服務器生成在 c:\shared-auth-ticket-keys 目錄下的 key 文件,拷貝到另外一臺服務器中,但還是沒用,過了很多天,有人回復了:

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.

大致意思是,雖然是同一個目錄,但會在不同服務器生成不同的 key 文件,所以身份驗證就不通過,解決方式是使用 key 共享文件,這樣讓不同服務器都能訪問同一個 key 文件,另外一種方式是將 key 存儲在一個地方,比如 SQL Server 中,但我不是很了解 key 的讀取和存儲方式,所以,我最后嘗試用第一種方式解決,只需要我們將目錄更改為共享目錄:

var dataProtection = new Microsoft.AspNet.DataProtection.DataProtectionProvider(new DirectoryInfo(@"\\10.10.10.10\shared-auth-ticket-keys\"));

后來再重新發布,還是出現了問題,比如共享文件放在一臺服務器上,這臺服務器訪問沒用什么問題,但另一臺服務器卻不能訪問,文件資源管理器可以訪問此共享文件,這個問題也折騰我很久,但不和 ASP.NET 5 相關,主要問題是不了解 ASP.NET 如何訪問共享文件,后來找資料解決了,記錄了一篇博文:ASP.NET 訪問共享文件夾

目前的情況:第一個問題已經實現,但是比較簡陋,開始考慮并實現第二個問題。

一開始的時候,我提了一個 Issue:Share ASP.NET MVC 5 Forms authentication?

這個 Issue 我覺得很有價值,它讓我了解了很多東西,比如 ASP.NET 5 不支持 Forms Authentication,ASP.NET 5 和 Forms Authentication 的 Cookie 加密方式不同,ASP.NET 5 會更加復雜,因為登錄系統不能被重寫,并且 ASP.NET 5 不支持 Forms Authentication,那么擺在我面前的只有一條路,在 ASP.NET 5 中,解密 Cookie(通過 Forms Authentication 加密),針對這個問題,我的一些想法:

其實看起來這個問題好像不是很復雜,通過 Key 加密生成 Cookie(Forms Authentication),然后通過下面方式獲取 Cookie(ASP.NET 5):

var cookies = Request.Cookies.First(x => x.Key == ".CNBlogsCookie").Value;

然后通過某些手段解密生成 IdentityUser 對象,對,沒錯,就這么簡單。

我們先不住 ASP.NET 5 中實現下,很簡單:

var cookies = "";
FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(cookies);
string[] roles = authTicket.UserData.Split(new char[] { ';' });
var user = new GenericPrincipal(User.Identity, roles);

這段代碼是執行成功的,但我們需要在 Web.config 中,配置如下代碼:

這段代碼必須要和登錄站點中的配置一樣,原因是加密和解密的方式要一一對應,接下來的工作,我們需要在 ASP.NET 5 中實現上面的代碼,但你會發現找不到 FormsAuthentication.Decrypt 了,這么辦呢?只能查看源碼,然后把相關代碼貼出來編譯一下,如果成功了(我嘗試了很多次,因為涉及的代碼太多,實現起來非常困難),這是第一步,第二步我們將編譯通過的代碼,放在 ASP.NET 5 中再編譯一次,這個工作我還沒做,不過看起來并不是那么簡單,因為運行時和基礎類庫都發生變化了。

如果重寫這部分代碼,我貼一下需要的一些資源(后面再嘗試下):

后來,上面那個 Issue 有人回復如下:

看到這,有點想哭的趕腳,但不管怎樣,還是要嘗試下,希望下集是一個成功的博文記錄,未完待續。。。

最后,貼一下這段時間累積的有關資料:


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()