坎坷路:ASP.NET 5 Identity 身份驗證(上集)

来源:http://www.cnblogs.com/xishuai/archive/2016/01/05/aspnet-5-identity-part-one.html
-Advertisement-
Play Games

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


之所以為上集,是因為我並沒有解決這個問題,寫這篇博文的目的是紀錄一下我所遇到的問題,以免自己忘記,其實已經忘了差不多了,寫的過程也是自己回顧的過程,並且之前收集有關 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 有人回覆如下:

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

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Define Proper ClassApplication ArchitectureDefine the components appropriately for the application and create project for each one.What is Class?3 Thi...
  • 引言:因為接觸過多個ORM,但使用的時候都遇到了各自的一些不夠理想的地方,從最早開始開始公司自己分裝的,到後面用EF,以及Dapper和DapperExtensions 到現在用的FluentData,就說說我自己的使用體驗,在這幾個相比之下,Dapper應該是最輕量級,而且性能也是最好的,但是相對...
  • 讀取枚舉特性小記
  • 直接上代碼:public static byte[] GetExecl(DataTable dt, List list) { var sbHtml = new StringBuilder(); sbHtml.Append(""); ...
  • 在花了不少時間研究學習了MongoDB資料庫的相關知識,以及利用C#對MongoDB資料庫的封裝、測試應用後,決定花一些時間來總結一下最近的研究心得,把這個資料庫的應用單獨作為一個系列來介紹,希望從各個方面來總結並記錄一下這個新型、看似神秘的資料庫使用過程。本文是這個系列的開篇,主要介紹一些Mong...
  • 今天第一次在博客園發帖,以前一直在潛水,在這裡也是學了不少東西。感謝各位園友廢話不多說,這也是我工作中遇到的問題:protected void Application_Start(object sender, EventArgs e) { // 創建一個計時器,單...
  • 下麵給大家分享一種通過 DataGridRowHeader 自動生成 DataGrid 數據行行號的方式。只需一個 ValueConverter 就能搞定。值轉換器 1 class AutoNumberValueConverter : IMultiValueConverter 2 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...