簡單了解 ASP.NET Core 的認證與授權


參考資料:

書籍《ASP.NET Core IN ACTION SECOND EDITION》ch14、ch15

0. 照例吐槽

這里我本來寫了一堆吐槽公司的話,但還是不放出來了,過於負能量。

但話說回來,我們當仆人的當然要服侍好我們的資本家主人們。就這樣,為了更好的被奴役,被榨取剩余價值,我今天要來了解一下 ASP.NET Core 的認證與授權。

1. 什么是認證 Authentication 和授權 Authorization

用外行都能聽懂的話來說:

  1. 認證 Authentication —— 為你的應用創建用戶並讓他們能夠登錄你的應用,即確認用戶是誰
  2. 授權 Authorization —— 控制當前登錄的用戶在的應用里可以做什么,不能做什么

用內行能聽懂的話來說:

  1. 認證 Authentication —— 確定是誰發出的請求
  2. 授權 Authorization —— 確定請求的行為是否被允許

2. ASP.NET Core 中的 users 和 claims

可能大家都看過官方文檔的默認教程,或者用 VS 創建過包含用戶系統的 Web 應用,他們都用了一個叫 Identity 的官方庫,在官方文檔中被翻譯為“標識”,笑死,根本看不懂,不如直接叫 Identity。

要明確一下,這個 Identity 中的 User 和 ASP.NET Core 的 user 不是同一個東西,他們是可以分開使用的。例如,有的公司只有一個用戶中心,所有系統都從這一個用戶中心取用戶信息,你當然可以在你的某個應用里用 Identity 再搭建維護一套用戶信息,但更多時候是從用戶中心取了直接用,沒有必要再在某個小應用里搞一套用戶信息了,也就沒有必要用 Identity 的 User 了。但你這個應用的認證和授權還是要進行,所以 ASP.NET Core 中的 user 和 claim 這時候就可以不依賴 Identity 而獨立地起作用了。

下面我們再來了解一下幾個東西:

Principal

不知道這玩意怎么翻譯,就不翻譯了。

可以直接把 Principal 當成你應用的用戶。

每一個對你應用的請求都會有一個 HttpContext,當前的 Principal 就會變成你 HttpContext 的 User 屬性。當你的應用需要知道當前的用戶是誰,他可以做什么,的時候,可以這樣訪問:HttpContext.User.Claims或者HttpContext.User.HasClaim(xxx),我也還沒試過,不確定是不是這個屬性,等學完理論去實戰的時候試試。

在 ASP.NET Core 中,Principals 的實現是 ClaimsPrincipals,它是一組跟它關聯的 claims 的集合。

截圖來自參考資料《ASP.NET Core IN ACTION SECOND EDITION》

就算我看不懂上圖里的字,我也能看懂這個圖。這顯然是說一個 ClaimsPrincipals 里包含了一組信息,包括用戶的 Email,家庭電話,FirstName,LastName,以及一個 HasAdminAccess 表示了該用戶擁有管理員權限。

這個圖一看我們就能明白,顯然 ClaimsPrincipals 里面放哪些項,是我們可以自定義的,畢竟上面連家庭電話這種項都出來了,現在誰家還用這玩意,還是不是90后啊?因此,我們可以定義一堆用戶信息和用戶權限存到數據庫里,與每個用戶在數據庫里的賬號對應起來,每次用戶登錄的時候就把這一堆東西從數據庫查出來,放進 ClaimsPrincipals 去。后面用戶的每一個請求都會把這個東西再以某種形式(cookie,header 等都行)帶回來,我們會在中間件管道中把這些東西添加到HttpContext.User,可能是HttpContext.User.Claims這個屬性,供我們的應用后面取用。

Claims

另外再提一嘴,上面圖里,把 ClaimsPrincipals 中的一項一項稱為 Claims,它包含一個類型(type)和一個值(value)。Claims 描述了這個 Principal 的屬性。所以我們可以把 Claims 理解成當前用戶的屬性。

上面的 Claims 里既有用戶信息如 Email,又有用戶的權限如 HasAdminAccess。由此我們確定權限和認證都跟這些 Claims 直接相關。

3. 認證和授權的抽象過程

看到這里,不管是用 ASP.NET Core 還是用什么別的語言別的框架,我們都能大體上知道怎么實現一個認證和授權的流程了。

  1. 用戶在客戶端輸入用戶名密碼登錄
  2. 服務端校驗用戶名和密碼,沒啥問題的話就從數據庫里取出用戶信息和權限,放到 HttpContext 里,隨時能取到。我們把這套東西抽象成 principal
  3. 這次請求結束時,我們把用戶的 principal 序列化,作為加密的 cookie 返回給瀏覽器。或者把這些信息加密放到一個 token 里,如 JWT 那種方式。然后返回給前端,前端想辦法存下來
  4. 前端再次發起請求時,就把這套信息繼續傳過來,我們解析,並放到 HttpContext 里隨時取用。

信息太長,或者擔心信息泄露怎么辦?我覺得可以直接存緩存里,比如 redis 里,隨便生成個 GUID 當 key,value 就是這套用戶信息和權限,當然要加密一下再存。把 key 返回給前端,前端以后每次請求都把這個 key 放 header 里帶過來,我們再從緩存里取出用戶信息和權限,這樣搞。緩存過期時間就是用戶這次登錄的失效時間。如果確定 key 被其它惡意用戶拿到,然后用它來冒充合法用戶,我們還能直接把他緩存清了,踢他下線。

Authentication 中間件和 Authorization 中間件在這個過程中發揮的作用

上面的 2、3、4 點應該是發生在 Authentication 中間件中,所以它后面的中間件能用到用戶信息。這就是負責授權的 Authorization 中間件要放在 Authentication 中間件后面的原因。放前面它壓根拿不到用戶信息和權限,拿什么去判斷用戶有沒有權限?

另外需要注意的是,Authentication 中間件不負責將未經身份驗證的請求重定向到登錄頁面,或者拒絕未經授權的請求。這兩個操作是由 Authorization 中間件處理的。

4. 如何處理未經授權的請求

未經授權的請求大概分為兩種:

  1. 未登錄的用戶發送的請求。這種請求沒有攜帶 Principal,說明用戶沒有登錄

  2. 用戶已經登錄,但沒有權限進行這項操作,或者調用這個 Web API。這種情況就值得討論一下了。可能要返回 401?或者其它的信息?

現在我們討論一下,可能有兩種響應(response):

  1. Challenge(質詢?考驗?挑戰?我也不知道怎么翻譯)—— 此響應表示用戶未被授權執行該操作,因為他們尚未登錄
  2. Forbid(禁止,阻止)—— 此響應表示用戶已登錄但不符合執行操作的要求。比如他們沒有執行這項操作需要的 claim

注意:在 ASP.NET Core 中,大家可能或多或少都知道可以用一個 Attribute 來給 Controller 或者 Action 加授權,這個 Attribute 就是[Authorize]。如果我們只用[Authorize]來修飾一個 Controller 或 Action,那么它只會校驗用戶是否登錄,只要登錄了的用戶,都可以執行操作。如何針對權限校驗?這個就更實戰一點了,本文主要是說理論和邏輯,這個后面會稍微提一嘴,剩下的等到實戰的文章里再說。

再看一下上面的兩種情況的處理方式。一般在 Web 應用中,觸發了 Challenge,用戶會被重定向到登錄頁面。而觸發了 Forbid,用戶可能要被重定向到應用定義的“禁止”或“訪問被拒絕”的頁面。

而在客戶端網頁應用,如用前后端分離的方法開發的基於 React 的前端 SPA,或者 Andriod、IOS 的移動端 APP,它們需要調用后端的 Web API 。這種情況下如果觸發了 Challenge 或者 Forbid,一般會被重定向到一個第三方的應用,這個應用能給 SPA 或者 APP 發 Token。例如你用某“小而美”的應用的賬號登錄某個網站,會跳轉回這個“小而美”的應用里,讓你點一下授權按鈕。但重定向到其他頁面不是由后端的 Web API 來做,Web API 可以對 Challenge 返回 401 Unauthorized,對 Forbid 返回 403 Forbidden。然后由客戶端來決定該怎么做。

什么是基於聲明(claims)的策略(policies)授權

認證結束后,我們開始考慮授權。我們現在可以想到兩種授權方式,一種是判斷用戶的 Principal 有沒有某個 Claim,另一種是判斷用戶的 Principal 中的某個 Claim 的值是不是某個特定的值。

在 ASP.NET Core 中,定義用戶是否獲得授權的規則被封裝在 policy 中。policy 定義了請求獲得授權必須滿足的要求。

而 policy 在 ASP.NET Core 中可以直接被加到一個 Controller 上或者一個 Action 上,還是用 [Authorize]這個 Attribute。差不多就像下面這樣:

[Authorize("CanHelloWorld")]
public class HelloWorldController : ControllerBase
{...}

如何添加 policy

上面那個CanHelloWorld的 policy 肯定不是憑空變出來的。他是在 Startup.cs 中的 ConfigureServices 方法中注冊的。

截圖來自參考資料《ASP.NET Core IN ACTION SECOND EDITION》

可以看到AddPolicy第一個參數是 policy 的名字,第二個委托的RequireClaim參數是這個 Policy 的規則規定了它需要哪些 claim。上面示例里它似乎只定義了一個 claim 的名字,claim 還支持鍵值對的定義方式。當然一個 policy 肯定能設置多個 claim。就是感覺用字符串比較不優雅,不知道有沒有更好的辦法。這個后面再看。

policyBuilder 支持的方法:

截圖來自參考資料《ASP.NET Core IN ACTION SECOND EDITION》

不知道全不全,書上就這些,感覺這些已經足夠定義一個簡單的權限系統了。

大體上翻譯一下,第一個方法表示只要登錄的用戶都滿足該 policy;第二個是給 policy 設置 claim,支持鍵值對的方式;第三個是需要 username 為方法參數里指定的 username 的用戶才滿足該 policy;第四個支持你用參數傳一個返回值為 bool 類型的委托進去,來定義更健壯的 policy,下面我們簡單講一下。

更復雜健壯的 policy

我們可以使用RequireAssertion方法定義更健壯的 policy。

結合時事和參考書里的例子,我們現在需要定義一個規則為用戶年滿 18 周歲的 policy。我們就可以傳一個委托進RequireAssertion方法,該委托判斷今天的日期減去用戶的 claim——DateOfBirth的 value 大於等於 18,就 return true,否則 return false, 即可實現這個稍微復雜的 policy 的規則。

寫到這里我發現,就看看這些理論,基本沒什么用,還得實戰一波。等我有空實戰一波再搞一篇博客。

Requirements 和 Handlers

我們再引入兩個概念,Requirements 和 Handlers。

每一個 policy 都包含一個或多個 requirenments,每一個 requirements 可以有一個或多個 handlers。參考書里講了一個例子。

如果你在飛機上,你想去洗手間。我們給洗手間定義一個 policy :("CanAccessLounge"),還有兩個 requirements : (MinimumAgeRequirement 和 AllowedInLoungeRequirement),還有幾個 handlers:

截圖來自參考資料《ASP.NET Core IN ACTION SECOND EDITION》

("CanAccessLounge")(能夠進入洗手間)這個 policy 有兩個 requirements,其中AllowedInLoungeRequirement(被允許進入洗手間)有三個 handlers,FrequentFlyerHandler需要你是乘機頻率很高的乘客,類似於航空公司的熟客;IsAirlineEmployeeHandler需要你是航空公司的員工;BannedFromLoungeHandler表示你是否被禁止使用飛機上的洗手間。而MinimumAgeRequirement(最小年齡限制)有一個 handler MinimumAgeRequirement,表示洗手間有最小的年齡限制。

所以,CanAccessLounge(能夠進入洗手間)必須AllowedInLoungeRequirement(被允許進入洗手間),而且滿足MinimumAgeRequirement(最小年齡限制)。

AllowedInLoungeRequirement(被允許進入洗手間),需要你滿足FrequentFlyerHandler(高頻率乘客)或IsAirlineEmployeeHandler(航空公司員工)。你不需要全部滿足這兩個 handlers,你只需要滿足其中一項。BannedFromLoungeHandler(被禁止使用洗手間)這個我們先不管。

現在,我們可以用 OR 來組合每個 requirement 的 handler,即只要滿足任何一個 handler,就滿足這個 requirement。我們也可以用 AND 來組合 requirenment,必須滿足所有的 requirement 才滿足這個 policy。而在 ASP.NET Core 中,我們可以給 Controller 或者 Action 設置多個 policy,就像這樣: [Authorize ("Policy1"), Authorize("Policy2")],必須滿足所有 policy 才能被授權。

所以現在的邏輯就如下圖:

截圖來自參考資料《ASP.NET Core IN ACTION SECOND EDITION》

簡單放幾段書里的代碼,看一下怎么創建這些 requirements 和 handlers:

  1. 創建AllowedInLoungeRequirement,注意要實現IAuthorizationRequirement接口。

  2. 創建MinimumAgeRequirement,這個 requirement 稍微特殊一點,它還帶有參數:

  3. 注冊包含這兩個 requirement 的 policy:

  4. 創建 handler 來滿足 requirement,可以看到需要繼承AuthorizationHandler<對應的 requirement>,覆寫它的HandleRequirementAsync方法:

上面兩個 handler 中調用context.User.HasClaim()來取 claim 時是稍有不同的,注意區別。

注意:可以編寫可用於多個 requirement 的通用的 handler,但最好每個 handler 只處理單個 requirement。如果需要提取一些通用功能,應該從兩個 requirement 調用它。

  1. 上面都是判斷 claims 有沒有滿足需要的條件,如果滿足了就context.Succeed(requirement);,但在開發過程中可能會有判斷 claims 滿足了某個條件反而不能通過授權的情況,那樣就要用到context.Fail();

  1. 這些 handler 也是需要注冊的:

    注意:上面這幾個 handler 沒有什么構造函數依賴項,所以注冊的生命周期全是 Singleton,如果我們的 handler 有什么生命周期為 scoped 或者 transient 的構造函數依賴項,比如依賴了 EFCore 的 DbContext,可能要考慮用 scoped 生命周期來注冊 handler

5. 總結

懶得總結了,就這樣吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM