欧美人与禽2O2O性论交,秋霞免费视频,国产美女视频免费观看网址,国产成人亚洲综合网色欲网

使用 ASP.NET Core 構(gòu)建 Web API:9 身份驗(yàn)證和授權(quán)

本章涵蓋

  • 了解身份驗(yàn)證和授權(quán)
  • 獲取 ASP.NET 核心標(biāo)識的概述
  • 通過用戶帳戶和 JSON Web 令牌實(shí)現(xiàn)身份驗(yàn)證
  • 使用 AuthorizeAttribute 和 IAuthorizationFilter 啟用授權(quán)
  • 了解基于角色的訪問控制 (RBAC) 授權(quán)策略

我們在前面幾章中構(gòu)建的 ASP.NET Core Web API 已經(jīng)成型。但是,在發(fā)布它之前,我們必須解決一些我們有意保持打開狀態(tài)的主要安全權(quán)限問題。如果我們仔細(xì)看看我們的BoardGamesController,DomainsController和MechanicsController,我們可以看到它們都有一些Post和Delete方法,任何人都可以用它來改變我們的寶貴數(shù)據(jù)。我們不希望這樣,是嗎?

出于這個(gè)原因,在考慮通過互聯(lián)網(wǎng)部署我們的 Web API 并使其可公開訪問之前,我們需要找到一種方法將這些方法的使用限制為有限的一組授權(quán)用戶。在本章中,我們將學(xué)習(xí)如何使用 ASP.NET Core Identity 來執(zhí)行此操作,核心標(biāo)識是一個(gè)內(nèi)置 API,可用于管理用戶、角色、聲明、令牌、策略、與授權(quán)相關(guān)的行為和其他功能。

9.1 基本概念

在深入研究代碼之前,最好先概述一下身份驗(yàn)證授權(quán)的概念。雖然這兩個(gè)術(shù)語經(jīng)常在同一上下文中使用,但它們具有不同、精確的含義。

9.1.1 身份驗(yàn)證

在信息安全中,身份驗(yàn)證是指驗(yàn)證計(jì)算機(jī)、軟件或用戶正確身份的行為。我們可以說,身份驗(yàn)證是一種驗(yàn)證實(shí)體(或個(gè)人)是它聲稱(或他們聲稱)的機(jī)制。

無論出于何種原因,身份驗(yàn)證過程對于需要唯一標(biāo)識其用戶的任何 Web 應(yīng)用或服務(wù)都至關(guān)重要 – 限制對部分(或全部)用戶數(shù)據(jù)的訪問、收集個(gè)人信息、記錄和/或跟蹤用戶在使用服務(wù)時(shí)的操作、注意他們是否已登錄、在某個(gè)非活動期后斷開它們, 等等。

此外,身份驗(yàn)證通常在增強(qiáng) Web 服務(wù)(及其背后的組織)的數(shù)據(jù)保護(hù)、安全性和監(jiān)視功能方面發(fā)揮重要作用。唯一地驗(yàn)證關(guān)聯(lián)主體的身份意味著系統(tǒng)內(nèi)執(zhí)行的所有操作都可以合理確定地追溯到其作者,從而促進(jìn)遵守組織的問責(zé)制政策。

問 責(zé)

問責(zé)制是 ISO/IEC 27001 的關(guān)鍵原則,ISO/IEC <> 是眾所周知的國際標(biāo)準(zhǔn),為組織內(nèi)設(shè)計(jì)、實(shí)施和運(yùn)營信息安全管理系統(tǒng)提供了系統(tǒng)的方法。

大多數(shù)歐盟隱私機(jī)構(gòu)也強(qiáng)調(diào)了身份驗(yàn)證和問責(zé)制之間的聯(lián)系。根據(jù)意大利數(shù)據(jù)保護(hù)局的說法,“. .共享憑據(jù)可防止在計(jì)算機(jī)系統(tǒng)中執(zhí)行的操作歸因于特定的負(fù)責(zé)人,也損害了所有者,剝奪了檢查此類相關(guān)技術(shù)人物工作的可能性“(第4/4/2019條)。

大多數(shù) Web 應(yīng)用、Web 服務(wù)和 IT 設(shè)備都要求其用戶在授予訪問權(quán)限之前完成某種身份驗(yàn)證過程。此過程可能涉及使用指紋解鎖我們的智能手機(jī),登錄Facebook或LinkedIn帳戶,在Instagram上發(fā)布照片 – 所有形式的身份驗(yàn)證過程,即使其中一些是在后臺執(zhí)行的,因?yàn)橛脩敉馑麄兊脑O(shè)備存儲他們的憑據(jù)并自動使用它們。

現(xiàn)在有幾種身份驗(yàn)證技術(shù)可用,例如用戶名(或電子郵件)和密碼;發(fā)送到電子郵件或移動設(shè)備的一次性 PIN 碼 (OTP);個(gè)人認(rèn)證應(yīng)用生成的一次性安全碼;以及指紋、視網(wǎng)膜和/或語音等生物特征掃描。我不會在本章中介紹所有這些技術(shù),但一些在線資源可以提供有關(guān)這些主題的更多信息。

提示有關(guān) ASP.NET Core 應(yīng)用中身份驗(yàn)證的更多詳細(xì)信息,請查看 http://mng.bz/jm6y。

9.1.2 授權(quán)

一般而言,授權(quán)是指行使、執(zhí)行或行使某些權(quán)利的許可或權(quán)力。在IT領(lǐng)域,授權(quán)被定義為系統(tǒng)能夠?qū)⒃L問權(quán)限(也稱為權(quán)限)分配給單個(gè)計(jì)算機(jī),軟件或用戶(或組)的過程。這些任務(wù)通常通過實(shí)現(xiàn)訪問策略、聲明或權(quán)限組來處理,這些策略、聲明或權(quán)限組允許或禁止一組給定邏輯空間(文件系統(tǒng)文件夾、驅(qū)動器網(wǎng)絡(luò)、數(shù)據(jù)庫、網(wǎng)站部分、Web API 終結(jié)點(diǎn)等)中的每個(gè)相關(guān)操作或活動(讀取、寫入、刪除等)。實(shí)際上,通常通過定義一系列訪問控制列表 (ACL) 來提供或拒絕授權(quán),這些列表指定

  • 特定資源允許的訪問類型(讀取、寫入、刪除等)
  • 授予或拒絕哪些計(jì)算機(jī)、軟件或用戶(或組)訪問權(quán)限

盡管授權(quán)是正交的并且獨(dú)立于身份驗(yàn)證,但這兩個(gè)概念本質(zhì)上是交織在一起的。如果系統(tǒng)無法識別其用戶,則無法將其與其ACL正確匹配,從而授予或拒絕對其資源的訪問權(quán)限。因此,大多數(shù)訪問控制機(jī)制都設(shè)計(jì)為同時(shí)要求身份驗(yàn)證和授權(quán)。更準(zhǔn)確地說,它們執(zhí)行以下操作:

  • 將盡可能低的授權(quán)權(quán)限分配給未經(jīng)身份驗(yàn)證的(匿名)用戶。這些權(quán)限通常包括訪問公共(無限制)內(nèi)容以及登錄頁面、模塊或表單。
  • 對成功執(zhí)行登錄嘗試的用戶進(jìn)行身份驗(yàn)證。
  • 檢查其 ACL 以將適當(dāng)?shù)脑L問權(quán)限(權(quán)限)分配給經(jīng)過身份驗(yàn)證的用戶。
  • 是否授權(quán)用戶訪問受限制的內(nèi)容,具體取決于授予他們或他們所屬的組的權(quán)限。

圖 9.1 描述了此方案中描述的身份驗(yàn)證和授權(quán)流。該圖模擬了具有一組只能由授權(quán)用戶訪問的資源的典型 Web 應(yīng)用程序的行為。

使用 ASP.NET Core 構(gòu)建 Web API:9 身份驗(yàn)證和授權(quán)

圖9.1 認(rèn)證授權(quán)流程

在圖中,身份驗(yàn)證過程應(yīng)在授權(quán)之前進(jìn)行,因?yàn)楹笳咝枰罢邅韴?zhí)行其工作。但這種情況不一定是真的。如果匿名用戶嘗試訪問受限資源,授權(quán)系統(tǒng)將在身份驗(yàn)證之前啟動,拒絕對未經(jīng)身份驗(yàn)證的用戶的訪問,并可能驅(qū)動 Web 應(yīng)用程序?qū)⒂脩糁囟ㄏ虻降卿涰撁?。在某些邊緣情況下,甚至可能存在只能由匿名用戶(未經(jīng)身份驗(yàn)證的用戶)訪問的資源。一個(gè)典型的示例是登錄頁面,因?yàn)樵谧N之前,絕不應(yīng)允許經(jīng)過身份驗(yàn)證的用戶執(zhí)行其他登錄嘗試。

注意將所有這些點(diǎn)連接起來,我們應(yīng)該看到身份驗(yàn)證和授權(quán)是不同的、獨(dú)立的和獨(dú)立的東西,即使它們最終是為了一起工作。即使授權(quán)可以在不知道連接方身份的情況下工作(只要它為未經(jīng)身份驗(yàn)證的用戶提供可行的 ACL),它也需要一個(gè)身份驗(yàn)證機(jī)制來完成其其余的工作。

現(xiàn)在我們已經(jīng)有了大致的了解,我們需要了解如何在 Web API 中實(shí)現(xiàn)可行的身份驗(yàn)證和授權(quán)機(jī)制。正如我們之前所了解的,在典型的 Web 應(yīng)用程序中,身份驗(yàn)證過程(通常由登錄階段表示)應(yīng)該在授權(quán)部分之前發(fā)生。當(dāng)用戶成功登錄后,我們將了解該用戶的權(quán)限并授權(quán)他們?nèi)ィɑ虿蝗ィ┤魏蔚胤健?/span>

但我們也(可能)知道HTTP協(xié)議是無狀態(tài)的。每個(gè)請求都是獨(dú)立執(zhí)行的,不知道之前執(zhí)行的請求。客戶端和服務(wù)器在請求/響應(yīng)周期內(nèi)執(zhí)行的所有操作(包括發(fā)送和/或接收的所有數(shù)據(jù))都將在響應(yīng)結(jié)束時(shí)丟失,除非客戶端和服務(wù)器配備了一些機(jī)制來將此數(shù)據(jù)存儲在某個(gè)位置。

注意這些機(jī)制不是 HTTP 協(xié)議的一部分,但它們通常利用其某些功能;換句話說,它們是建立在它之上的。很好的例子是我們在第8章中看到的緩存技術(shù),它可以在客戶端和/或服務(wù)器端實(shí)現(xiàn)。這些技術(shù)使用一組特定的 HTTP 標(biāo)頭(如緩存控制)來指示緩存服務(wù)要執(zhí)行的操作。

如果我們將這兩個(gè)事實(shí)聯(lián)系起來,我們會看到我們遇到了一個(gè)問題:如果每個(gè)請求都不知道之前發(fā)生了什么,我們?nèi)绾沃烙脩羰欠褚淹ㄟ^身份驗(yàn)證?我們?nèi)绾胃櫽傻卿洷韱斡|發(fā)的請求/響應(yīng)周期的結(jié)果,即登錄結(jié)果和(如果成功)用戶的身份?下一節(jié)簡要介紹一些解決此問題的方法。

實(shí)現(xiàn)方法

在現(xiàn)代 Web 服務(wù)和應(yīng)用程序中設(shè)置 HTTP 身份驗(yàn)證的最常用方法是會話/cookie、持有者令牌、API 密鑰、簽名和證書。這些技術(shù)中的大多數(shù)不需要普通Web開發(fā)人員的介紹,但是花一些時(shí)間描述它們的工作原理可能是明智的:

  • 會話/Cookie – 此方法依賴于鍵/值存儲服務(wù),通常位于 Web 服務(wù)器或外部服務(wù)器或集群上。Web 應(yīng)用程序使用此服務(wù)來存儲用戶身份驗(yàn)證信息(會話),并為其分配自動生成的唯一 sessionId。然后,sessionId 通過 cookie 發(fā)送到瀏覽器,以便在所有后續(xù)請求中重新發(fā)送,并在服務(wù)器上用于檢索用戶的會話并以無縫、透明的方式采取相應(yīng)的行動(執(zhí)行基于授權(quán)的檢查)。
  • 持有者令牌 – 此方法依賴于身份驗(yàn)證服務(wù)器生成并包含相關(guān)授權(quán)信息的加密令牌。此令牌將發(fā)送到客戶端,客戶端可以通過在授權(quán) HTTP 標(biāo)頭中設(shè)置令牌來使用它來執(zhí)行后續(xù)請求(直到過期),而無需進(jìn)一步的身份驗(yàn)證嘗試。
  • API 密鑰 – 運(yùn)行 Web API 的服務(wù)為其用戶提供可用于訪問 API 的 ClientID 和 CLIentSecret 對(或讓他們有機(jī)會生成它們)。通常,該對在每個(gè)請求時(shí)通過授權(quán) HTTP 標(biāo)頭發(fā)送。但是,與不需要身份驗(yàn)證的持有者令牌不同(稍后會詳細(xì)介紹),ClientID 和 ClientSecret 通常用于每次對請求用戶進(jìn)行身份驗(yàn)證,以及授權(quán)該用戶。
  • 簽名和證書 – 這兩種身份驗(yàn)證方法使用以前共享的私鑰和/或傳輸層安全性 (TLS) 證書執(zhí)行請求的哈希。此技術(shù)可確保沒有入侵者或中間人可以充當(dāng)請求方,因?yàn)樗麄儗o法“簽署”HTTP 請求。這些方法對于安全性非常有用,但對于雙方來說,它們可能很難設(shè)置和實(shí)施,這限制了它們對需要特別高的數(shù)據(jù)保護(hù)標(biāo)準(zhǔn)的服務(wù)。

我們應(yīng)該為我們的MyBGList Web API使用以下哪種方法?與往常一樣,我們應(yīng)該考慮每種選擇的利弊。以下是快速細(xì)分:

  • 會話/cookie顯然不在圖片之外,因?yàn)樗鼈儠穸ㄎ覀兊?span id="o3ebtxw" class="candidate-entity-word" data-gid="8794668">RESTful目的,例如我們自第3章以來就知道的無狀態(tài)約束。
  • 持有者令牌提供了不錯(cuò)的安全態(tài)勢,并且易于實(shí)現(xiàn),特別是考慮到 ASP.NET 核心身份(幾乎)開箱即用地支持它們。
  • API 密鑰提供了更好的安全態(tài)勢,但它們需要大量額外的工作,例如提供專用的管理網(wǎng)站或 API 集,以使用戶能夠正確管理它們。
  • 從安全角度來看,簽名和證書很棒,但它們需要更多的額外工作,這可能會導(dǎo)致我們出現(xiàn)一些延遲和/或增加總體成本。

因?yàn)槲覀兲幚淼氖瞧灞P游戲,而不是敏感數(shù)據(jù),所以至少從成本/收益的角度來看,持有者代幣方法似乎是我們最好的選擇。這種選擇的好處是,它共享了實(shí)現(xiàn) API 密鑰方法所需的大部分工作。這是學(xué)習(xí) ASP.NET 核心標(biāo)識基本技術(shù)并通過為大多數(shù) Web API 構(gòu)建可行的身份驗(yàn)證和授權(quán)機(jī)制將其付諸實(shí)踐的絕佳機(jī)會。下一節(jié)介紹持有者令牌的工作原理。

警告本章及其源代碼示例的主要目的是概述可用于 Web API 的各種身份驗(yàn)證和授權(quán)機(jī)制,并就如何使用 ASP.NET 核心標(biāo)識實(shí)現(xiàn)其中一些機(jī)制提供一般指導(dǎo)。但是,了解這些方法是黑客攻擊、拒絕服務(wù) (DoS) 攻擊以及第三方執(zhí)行的其他一些惡意活動的主要目標(biāo)至關(guān)重要,這些活動可以輕松利用陷阱、實(shí)現(xiàn)錯(cuò)誤、未更新的庫、零日錯(cuò)誤等。因此,如果你的 Web API 和/或其基礎(chǔ)數(shù)據(jù)源包含個(gè)人、敏感或有價(jià)值的數(shù)據(jù),請考慮通過使用我隨它們提供的安全相關(guān)超鏈接以及有關(guān)每個(gè)主題的其他權(quán)威教程來集成或改進(jìn)我們的代碼示例來加強(qiáng)安全狀況。

不記名令牌

基于令牌的身份驗(yàn)證(也稱為持有者身份驗(yàn)證)是 Web API 最常用的方法之一。如果實(shí)施得當(dāng),它可以在不破壞無狀態(tài) REST 約束的情況下提供可接受的安全標(biāo)準(zhǔn)。

基于令牌的身份驗(yàn)證仍要求用戶使用用戶名和密碼對自己進(jìn)行身份驗(yàn)證(執(zhí)行登錄)。但是,身份驗(yàn)證過程成功后,服務(wù)器不會創(chuàng)建持久會話,而是生成一個(gè)加密的授權(quán)令牌,其中包含有關(guān)結(jié)果的一些相關(guān)信息,例如對用戶標(biāo)識 (userId) 的引用、有關(guān)連接客戶端的一些信息、令牌到期日期等。此令牌一旦被客戶端檢索,就可以在任何后續(xù)請求的授權(quán) HTTP 標(biāo)頭中設(shè)置,以獲取對受限(授權(quán))資源的訪問權(quán)限,直到過期。圖9.2總結(jié)了這一過程。

使用 ASP.NET Core 構(gòu)建 Web API:9 身份驗(yàn)證和授權(quán)

圖9.2 持有者令牌授權(quán)流程

如我們所見,服務(wù)器不存儲任何數(shù)據(jù)。至于客戶端,實(shí)現(xiàn)可能會有所不同:令牌可以存儲在本地(并重復(fù)使用直到過期)或在首次使用后丟棄。持有者令牌的主要優(yōu)點(diǎn)是它們是一種獨(dú)立的授權(quán)機(jī)制,因?yàn)樗鼈兊拇嬖跁詣右馕吨矸蒡?yàn)證嘗試成功。單個(gè)令牌可用于授權(quán)發(fā)往多個(gè) Web API 和/或服務(wù)的受限請求,即使它們托管在其他地方和/或無法訪問用戶登錄數(shù)據(jù),只要它們共享生成它們的身份驗(yàn)證服務(wù)使用的相同頒發(fā)者簽名密鑰。

注意這種多功能性(和性能優(yōu)勢)也是主要安全漏洞的原因:代幣發(fā)行后,它們不能輕易失效(或更新)。如果第三方設(shè)法竊取和使用令牌,他們將能夠執(zhí)行授權(quán)請求,直到令牌過期。此外,開發(fā)人員、系統(tǒng)管理員和用戶無法輕松擺脫該令牌,即使他們知道它已被泄露。即使禁用原始用戶也無法解決問題,因?yàn)樵摿钆剖窃撚脩羧蕴幱诨顒訝顟B(tài)時(shí)發(fā)生的身份驗(yàn)證過程的結(jié)果。此安全問題的最佳解決方法是盡可能縮短這些令牌的生命周期(理想情況下,縮短到幾分鐘),以便攻擊者沒有太多時(shí)間采取行動。

現(xiàn)在我們已經(jīng)為具體方案選擇了一條路徑并了解了它應(yīng)該如何工作,是時(shí)候熟悉我們將用于實(shí)現(xiàn)它的框架了。

9.2 ASP.NET 核心身份

ASP.NET 核心標(biāo)識 API 提供了一組接口和高級抽象,可用于在任何 ASP.NET 核心應(yīng)用中管理和存儲用戶帳戶。盡管它可以與任何數(shù)據(jù)庫和/或對象關(guān)系映射/映射器 (ORM) 一起使用,但該框架已經(jīng)提供了多個(gè)類、幫助程序和擴(kuò)展方法,允許我們將其所有功能與實(shí)體框架核心 (EF Core) 數(shù)據(jù)模型一起使用,這使其非常適合我們當(dāng)前的方案。

注意ASP.NET 核心身份源代碼是開源的,可在 GitHub 上找到 http://mng.bz/WAmx

在以下部分中,我們將學(xué)習(xí)如何使用 ASP.NET 核心身份為我們現(xiàn)有的 MyBGList Web API 項(xiàng)目提供身份驗(yàn)證功能。(接下來將進(jìn)行授權(quán)。為此,我們將執(zhí)行以下步驟:

  1. 安裝所需的 NuGet 包。
  2. 創(chuàng)建一個(gè)新的 MyBGListUser 實(shí)體類來處理用戶名和密碼等用戶數(shù)據(jù)。
  3. 更新我們現(xiàn)有的 ApplicationDbContext,使其能夠處理新的用戶實(shí)體。
  4. 添加并應(yīng)用新遷移,以使用核心標(biāo)識所需的數(shù)據(jù)庫表更新基礎(chǔ)數(shù)據(jù)庫 ASP.NET。
  5. 在程序.cs文件中設(shè)置和配置所需的標(biāo)識服務(wù)和中間件。
  6. 實(shí)現(xiàn)新控制器來處理注冊過程(創(chuàng)建新用戶)和登錄過程(將臨時(shí)訪問令牌分配給現(xiàn)有用戶)。

9.2.1 安裝 NuGet 包

若要將 ASP.NET 核心標(biāo)識功能添加到項(xiàng)目中,我們需要以下 NuGet 包:

  • Microsoft.Extensions.Identity.Core,包含成員系統(tǒng)以及處理我們需要的各種登錄功能的主要類和服務(wù)
  • Microsoft.ASPNetCore.Identity.EntityFrameworkCore,EF Core 的 ASP.NET Core Identity 提供程序
  • Microsoft.AspNetCore.Authentication.JwtBearer,包含使 ASP.NET 核心應(yīng)用程序能夠處理JSON Web令牌(JWT)的中間件

與往常一樣,我們可以選擇使用 NuGet 包管理器或包管理器控制臺在 Visual Studio 中安裝所需的 NuGet 包,或者使用 .NET Core 命令行界面 (CLI) 從命令行安裝所需的 NuGet 包。若要使用 CLI,請打開命令提示符,導(dǎo)航到項(xiàng)目的根文件夾,然后鍵入以下命令:

> dotnet add package Microsoft.Extensions.Identity.Core --version 6.0.11> dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore --? version 6.0.11> dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --? version 6.0.11

現(xiàn)在我們可以開始編寫一些東西,從我們在第 4 章中創(chuàng)建的 ApplicationDbContext 類開始。

9.2.2 創(chuàng)建用戶實(shí)體

現(xiàn)在我們已經(jīng)安裝了標(biāo)識包,我們需要?jiǎng)?chuàng)建一個(gè)新的實(shí)體類,表示我們要進(jìn)行身份驗(yàn)證和授權(quán)的用戶。此實(shí)體的名稱將為 ApiUser。

注意理想情況下,我們可以稱這個(gè)實(shí)體為User,但該通用名稱會與其他內(nèi)置屬性(如ControllerBase.User)產(chǎn)生一些令人討厭的沖突。為了避免這個(gè)問題,我強(qiáng)烈建議選擇一個(gè)更獨(dú)特的名稱。

因?yàn)槲覀兪褂玫氖?ASP.NET 核心身份,所以我們可以實(shí)現(xiàn)新實(shí)體的最好辦法是擴(kuò)展框架提供的默認(rèn)實(shí)現(xiàn)來處理由 IdentityUser 類(Microsoft的一部分)表示的身份用戶。AspNetCore.Identity 命名空間)。創(chuàng)建一個(gè)新的 /Model/ApiUser.cs 類文件,并使用以下代碼填充該文件:

using Microsoft.AspNetCore.Identity; namespace MyBGList.Models{ public class ApiUser : IdentityUser { }}

就這樣。我們現(xiàn)在不需要實(shí)現(xiàn)更多的東西,因?yàn)?IdentityUser 類已經(jīng)包含我們需要的所有屬性:用戶名、密碼等。

提示由于篇幅原因,我不會提供對 IdentityUser 默認(rèn)類的廣泛描述。若要了解有關(guān)它(及其屬性)的詳細(xì)信息,請參閱 http://mng.bz/8182 中的定義。

現(xiàn)在我們有一個(gè)專用的實(shí)體來處理我們的用戶,我們可以更新我們的 ApplicationDbContext 類以充分利用它。

9.2.3 更新應(yīng)用程序數(shù)據(jù)庫上下文

在第 4 章中,當(dāng)我們創(chuàng)建 ApplicationDbContext 類時(shí),我們擴(kuò)展了 DbContext 基類。為了使它能夠處理我們新的 ApiUser 實(shí)體,我們需要使用另一個(gè)基類來更改它,該基類包含我們需要 ASP.NET 核心標(biāo)識功能。這個(gè)基類的名稱是(你可能猜到的)IdentityDbContext,它是我們之前安裝的Microsoft.AspNetCore.Identity.EntityFrameworkCore NuGet包的一部分。以下是我們?nèi)绾巫龅竭@一點(diǎn)(更新的代碼以粗體顯示):

using Microsoft.AspNetCore.Identity.EntityFrameworkCore; ? // ... existing code public class ApplicationDbContext : IdentityDbContext<ApiUser> ?

? 必需的命名空間

? 新的 IdentityDbContext<TUser> 基類

請注意,新的基類需要一個(gè) TUser 類型的對象,該對象必須是 IdentityUser 類型的類。在此處指定我們的 ApiUser 實(shí)體指示由 ASP.NET Core 標(biāo)識擴(kuò)展包提供支持的 EF Core 在其上使用其標(biāo)識功能。

9.2.4 添加和應(yīng)用新遷移

現(xiàn)在,我們已經(jīng)使應(yīng)用程序數(shù)據(jù)庫上下文知道了我們的新用戶實(shí)體,我們準(zhǔn)備添加新的遷移來更新基礎(chǔ) SQL Server 數(shù)據(jù)庫,使用我們在第 4 章中學(xué)習(xí)的代碼優(yōu)先方法創(chuàng)建 ASP.NET 核心標(biāo)識所需的數(shù)據(jù)庫表。打開新的命令提示符,導(dǎo)航到 MyBGList 項(xiàng)目的根文件夾,然后鍵入以下內(nèi)容以創(chuàng)建新的遷移:

> dotnet ef migrations add Identity

然后鍵入以下命令以將遷移應(yīng)用到我們的 MyBGList 數(shù)據(jù)庫:

> dotnet ef database update Identity

如果一切順利,CLI 命令應(yīng)顯示文本,記錄兩個(gè)任務(wù)的成功結(jié)果。我們可以通過打開 SQL Server Management StudioSSMS) 來仔細(xì)檢查結(jié)果,以查看是否已創(chuàng)建新的 ASP.NET 核心標(biāo)識表。預(yù)期結(jié)果如圖9.3所示。

使用 ASP.NET Core 構(gòu)建 Web API:9 身份驗(yàn)證和授權(quán)

圖9.3 ASP.NET 核心標(biāo)識表

根據(jù) ASP.NET 核心標(biāo)識默認(rèn)行為,所有標(biāo)識數(shù)據(jù)庫表都有一個(gè) AspNet 前綴,這通常是一件好事,因?yàn)樗试S我們輕松地將它們與其他表區(qū)分開來。

管理遷移(以及處理基于遷移的錯(cuò)誤)

遷移功能是 EF Core 的獨(dú)特優(yōu)勢之一,因?yàn)樗试S開發(fā)人員以增量方式更新數(shù)據(jù)庫架構(gòu),使其與應(yīng)用程序的數(shù)據(jù)模型保持同步,同時(shí)保留數(shù)據(jù)庫中的現(xiàn)有數(shù)據(jù),以及隨時(shí)回滾到以前的狀態(tài),就像我們對源代碼管理所做的那樣。但從長遠(yuǎn)來看,此功能可能很難維護(hù),特別是如果我們意外刪除了 dotnet-ef 工具生成的增量文件之一。發(fā)生這種情況時(shí),任何使用 CLI 更新現(xiàn)有數(shù)據(jù)庫架構(gòu)的嘗試都可能會返回 SQL 錯(cuò)誤,例如“表/列/鍵已存在”。避免看到此錯(cuò)誤消息的唯一方法是保留所有遷移文件。這就是為什么我們遵循在項(xiàng)目內(nèi)部的文件夾中生成它們的良好做法,確保它們與其余代碼一起置于源代碼管理之下。

盡管有這些對策,但在某些邊緣情況下,遷移的增量機(jī)制可能會不可挽回地中斷;我們將無法恢復(fù)和/或回滾到安全狀態(tài)。每當(dāng)發(fā)生這種情況時(shí),或者如果我們丟失了遷移文件而無法恢復(fù)它,我們能做的最好的事情就是重置所有遷移并創(chuàng)建一個(gè)與我們當(dāng)前數(shù)據(jù)庫架構(gòu)同步的新遷移。這個(gè)過程涉及一些手工工作,稱為擠壓,并在 http://mng.bz/Eljl 的Microsoft官方指南中進(jìn)行了詳細(xì)解釋。

如果我們想更改表名,我們可以通過重寫 ApplicationDbContext 的 OnModelCreate 方法中的默認(rèn)值來實(shí)現(xiàn),如下所示(但不要在代碼中執(zhí)行此操作):

modelBuilder.Entity<ApiUser>().ToTable("ApiUsers");modelBuilder.Entity<IdentityRole<string>>().ToTable("ApiRoles");modelBuilder.Entity<IdentityRoleClaim<string>>().ToTable("ApiRoleClaims");modelBuilder.Entity<IdentityUserClaim<string>>().ToTable("ApiUserClaims");modelBuilder.Entity<IdentityUserLogin<string>>().ToTable("ApiUserLogins");modelBuilder.Entity<IdentityUserRole<string>>().ToTable("ApiRoles");modelBuilder.Entity<IdentityUserToken<string>>().ToTable("ApiUserTokens");

此代碼會將 AspNet 前綴替換為 Api。但我們不會在代碼示例中執(zhí)行此操作;我們將保留默認(rèn)前綴。

9.2.5 設(shè)置服務(wù)和中間件

現(xiàn)在我們需要在我們的程序.cs文件中設(shè)置和配置一些服務(wù)和中間件。我們需要添加以下內(nèi)容:

  • 身份服務(wù) – 執(zhí)行注冊和登錄過程
  • 授權(quán)服務(wù) – 定義頒發(fā)和讀取 JWT 的規(guī)則
  • 身份驗(yàn)證中間件 – 將 JWT 讀取任務(wù)添加到 HTTP 管道

讓我們從標(biāo)識服務(wù)開始。

添加身份服務(wù)

以下是我們需要做的:

  1. 將 ASP.NET 核心標(biāo)識服務(wù)添加到服務(wù)容器。
  2. 配置用戶密碼的最低安全要求(也稱為密碼強(qiáng)度)。
  3. 添加 ASP.NET 身份驗(yàn)證中間件。

打開 Program.cs 文件,找到我們將 DbContext 添加到服務(wù)容器的部分,并在它下面添加清單 9.1 中的代碼(粗體新行)。

清單 9.1 程序.cs文件:標(biāo)識服務(wù)

using Microsoft.AspNetCore.Identity; ? builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( builder.Configuration.GetConnectionString("DefaultConnection")) ); builder.Services.AddIdentity<ApiUser, IdentityRole>(options => ?{ options.Password.RequireDigit = true; ? options.Password.RequireLowercase = true; ? options.Password.RequireUppercase = true; ? options.Password.RequireNonAlphanumeric = true; ? options.Password.RequiredLength = 12; ?}) .AddEntityFrameworkStores<ApplicationDbContext>();

? 必需的命名空間

? 添加身份服務(wù)

? 配置密碼強(qiáng)度要求

如我們所見,我們告訴 ASP.NET 標(biāo)識僅接受具有以下特征的密碼

  • 至少一個(gè)小寫字母
  • 至少一個(gè)大寫字母
  • 至少一個(gè)數(shù)字字符
  • 至少一個(gè)非字母數(shù)字字符
  • 至少 12 個(gè)字符

這些安全標(biāo)準(zhǔn)將為我們的用戶提供非數(shù)據(jù)敏感方案的良好級別的身份驗(yàn)證安全性。下一步是設(shè)置身份驗(yàn)證服務(wù)。

添加身份驗(yàn)證服務(wù)

在我們的方案中,身份驗(yàn)證服務(wù)具有以下用途:

  • 將 JWT 定義為默認(rèn)身份驗(yàn)證方法
  • 啟用 JWT 持有者身份驗(yàn)證方法
  • 設(shè)置 JWT 驗(yàn)證、頒發(fā)和生存期設(shè)置

下面的清單包含相關(guān)代碼,我們可以將其放在標(biāo)識服務(wù)正下方的程序.cs文件中。

清單 9.2 程序.cs文件:認(rèn)證服務(wù)

using Microsoft.AspNetCore.Authentication.JwtBearer; ?using Microsoft.IdentityModel.Tokens; ? builder.Services.AddAuthentication(options => { ? options.DefaultAuthenticateScheme = options.DefaultChallengeScheme = options.DefaultForbidScheme = options.DefaultScheme = options.DefaultSignInScheme = options.DefaultSignOutScheme = JwtBearerDefaults.AuthenticationScheme; ?}).AddJwtBearer(options => { ? options.TokenValidationParameters = new TokenValidationParameters ? { ValidateIssuer = true, ValidIssuer = builder.Configuration["JWT:Issuer"], ValidateAudience = true, ValidAudience = builder.Configuration["JWT:Audience"], ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( System.Text.Encoding.UTF8.GetBytes( builder.Configuration["JWT:SigningKey"]) ) };});

? 必需的命名空間

? 添加身份驗(yàn)證服務(wù)

? 設(shè)置默認(rèn)授權(quán)相關(guān)方案

? 添加 JWT 持有者身份驗(yàn)證方案

? 配置 JWT 選項(xiàng)和設(shè)置

JWT 持有者選項(xiàng)部分是代碼中最有趣的部分,因?yàn)樗鼪Q定了身份驗(yàn)證服務(wù)應(yīng)如何驗(yàn)證令牌。如我們所見,我們要求驗(yàn)證頒發(fā)者、受眾和頒發(fā)者用于對令牌進(jìn)行簽名的密鑰 (IssuerSigningKey)。執(zhí)行這些檢查將大大減少惡意第三方頒發(fā)或偽造有效令牌的機(jī)會。

請注意,我們沒有直接在代碼中指定這些參數(shù),而是使用了對配置文件的引用。我們現(xiàn)在需要更新這些文件,以便源代碼能夠檢索這些值。

更新 appsettings.json 文件

打開 appsettings.json 文件,并在現(xiàn)有 SeriLog 項(xiàng)的正下方添加以下頂級部分:

"JWT": { "Issuer": "MyBGList", "Audience": "MyBGList", "SigningKey": "MyVeryOwnTestSigningKey123$" }

與往常一樣,如果計(jì)劃在可公開訪問的生產(chǎn)環(huán)境中部署 Web API,請務(wù)必使用自己的值更改示例值。

提示在 secret.json 文件中移動簽名密鑰將確保更好的安全態(tài)勢。請務(wù)必執(zhí)行此操作,除非你正在處理像這樣的示例應(yīng)用。

現(xiàn)在我們的服務(wù)已經(jīng)正確設(shè)置,我們幾乎完成了程序.cs文件?,F(xiàn)在缺少的只是身份驗(yàn)證中間件。

添加身份驗(yàn)證中間件

在 Program.cs 文件中,向下滾動到現(xiàn)有行

app.UseAuthorization();

并在其前面添加 ASP.NET Core 身份驗(yàn)證中間件:

app.UseAuthentication(); ?app.UseAuthorization(); ?

? 新的身份驗(yàn)證中間件

? 現(xiàn)有授權(quán)中間件

從第2章開始,我們就知道中間件順序很重要,因?yàn)橹虚g件按順序影響HTTP請求管道。因此,請確保在 UseAuthorization() 之前調(diào)用 UseAuthentication(),因?yàn)槲覀兊膽?yīng)用需要知道使用哪種身份驗(yàn)證方案和處理程序來授權(quán)請求。現(xiàn)在,我們已經(jīng)設(shè)置并配置了 ASP.NET Core Identity 服務(wù)和身份驗(yàn)證中間件,我們已準(zhǔn)備好實(shí)現(xiàn)用戶將用于創(chuàng)建其帳戶(注冊)然后對自己進(jìn)行身份驗(yàn)證(登錄)的操作方法。

9.2.6 實(shí)現(xiàn)帳戶控制器

在本節(jié)中,我們將創(chuàng)建一個(gè)新的AccountController,并使用兩種操作方法填充它:注冊(創(chuàng)建新用戶)和登錄(對其進(jìn)行身份驗(yàn)證)。這兩種方法都需要一些必需的輸入?yún)?shù)才能執(zhí)行其工作。例如,Register 方法需要想要?jiǎng)?chuàng)建帳戶的用戶的數(shù)據(jù)(用戶名、密碼、電子郵件等),而 Login 方法只需要知道用戶名和密碼。由于帳戶控制器必須處理一些與核心標(biāo)識相關(guān)的特定 ASP.NET 任務(wù),因此我們將需要以下以前從未使用過的服務(wù):

  • 用戶管理器 – 提供用于管理用戶的 API
  • 登錄管理器 – 提供用于登錄用戶的 API

這兩個(gè)服務(wù)都是 Microsoft.AspNetCore.Identity 命名空間的一部分。我們將需要第一個(gè)用于注冊方法,第二個(gè)用于處理登錄。此外,因?yàn)槲覀冞€需要讀取我們在appsettings.json配置文件中指定的JWT設(shè)置,所以我們也需要IConfiguration接口。與往常一樣,所有這些依賴項(xiàng)都將通過依賴項(xiàng)注入提供。

讓我們從控制器本身中的空樣板開始。在項(xiàng)目的 /Controllers/ 文件夾中創(chuàng)建一個(gè)新的 AccountController.cs C# 類文件,并使用以下清單中的代碼填充該文件。

清單 9.3 帳戶控制器樣板

using Microsoft.AspNetCore.Mvc;using Microsoft.EntityFrameworkCore;using MyBGList.DTO;using MyBGList.Models;using System.Linq.Expressions;using System.Linq.Dynamic.Core;using System.ComponentModel.DataAnnotations;using MyBGList.Attributes;using System.Diagnostics;using Microsoft.AspNetCore.Identity; ?using Microsoft.IdentityModel.Tokens; ?using System.IdentityModel.Tokens.Jwt; ?using System.Security.Claims; ? namespace MyBGList.Controllers{ [Route("[controller]/[action]")] ? [ApiController] public class AccountController : ControllerBase { private readonly ApplicationDbContext _context; private readonly ILogger<DomainsController> _logger; private readonly IConfiguration _configuration; private readonly UserManager<ApiUser> _userManager; ? private readonly SignInManager<ApiUser> _signInManager; ? public AccountController( ApplicationDbContext context, ILogger<DomainsController> logger, IConfiguration configuration, UserManager<ApiUser> userManager, ? SignInManager<ApiUser> signInManager) ? { _context = context; _logger = logger; _configuration = configuration; _userManager = userManager; ? _signInManager = signInManager; ? } [HttpPost] [ResponseCache(CacheProfileName = "NoCache")] public async Task<ActionResult> Register() ? { throw new NotImplementedException(); } [HttpPost] [ResponseCache(CacheProfileName = "NoCache")] public async Task<ActionResult> Login() ? { throw new NotImplementedException(); } }}

? ASP.NET 核心身份命名空間

? 路由屬性

? 用戶管理器接口

? 登錄管理器接口

? 注冊方式

? 登錄方式

請注意,我們已經(jīng)使用基于操作的路由規(guī)則(“[控制器]/[操作]”)定義了一個(gè) [Route] 屬性,因?yàn)槲覀儽仨毺幚硇枰獏^(qū)分的兩個(gè) HTTP POST 方法。由于該規(guī)則,我們的方法將具有以下端點(diǎn):

/Account/Register/Account/Login

除此之外,我們還為 _userManager、_signInManager 和 _configuration 對象(通過依賴注入)設(shè)置了一個(gè)本地實(shí)例,并創(chuàng)建了兩個(gè)未實(shí)現(xiàn)的方法。在以下部分中,我們將從 Register 開始實(shí)現(xiàn)這兩種方法(及其 DTO)。

實(shí)現(xiàn)寄存器方法

如果我們查看 ASP.NET SQL Server數(shù)據(jù)庫中為我們創(chuàng)建的核心標(biāo)識的[AspNetUsers]表,我們會看到創(chuàng)建新用戶所需的參數(shù)(圖9.4)。

使用 ASP.NET Core 構(gòu)建 Web API:9 身份驗(yàn)證和授權(quán)

圖 9.4 AspNetUsers 數(shù)據(jù)庫表

此表用于存儲我們之前創(chuàng)建的 ApiUser 實(shí)體的記錄,該實(shí)體是 IdentityUser 默認(rèn)類的擴(kuò)展。如果我們檢查該實(shí)體,我們會看到它對每個(gè)表列都有一個(gè)公共屬性,這并不奇怪,因?yàn)槲覀兪紫仁褂昧?EF Core 代碼優(yōu)先方法來創(chuàng)建表。

現(xiàn)在我們知道了我們需要從想要?jiǎng)?chuàng)建新帳戶的用戶那里獲取的數(shù)據(jù),我們可以實(shí)現(xiàn) DTO 對象來“傳輸”他們,從而將第 6 章中的課程付諸實(shí)踐。在項(xiàng)目的 /DTO/ 文件夾中創(chuàng)建一個(gè)新的 RegisterDTO.cs C# 類文件,并用清單 9.4 中所示的代碼填充該文件。為簡單起見,我們將要求注冊用戶向我們發(fā)送三種類型的信息:有效的用戶名、他們想要用于執(zhí)行登錄的密碼以及他們的電子郵件地址。

清單 9.4 注冊DTO類

using System.ComponentModel.DataAnnotations; namespace MyBGList.DTO{ public class RegisterDTO { [Required] public string? UserName { get; set; } [Required] [EmailAddress] public string? Email { get; set; } [Required] public string? Password { get; set; } }}

現(xiàn)在我們有了DTO,我們可以使用它來實(shí)現(xiàn)我們的 帳戶控制器 。注冊方法,預(yù)期處理以下任務(wù):

  1. 接受寄存器DTO輸入。
  2. 檢查模型狀態(tài)以確保輸入有效。
  3. 如果 ModelState 有效,則創(chuàng)建一個(gè)新用戶(記錄結(jié)果),并返回狀態(tài)代碼 201 – 已創(chuàng)建;否則,返回狀態(tài)代碼 400 – 記錄錯(cuò)誤的錯(cuò)誤請求。
  4. 如果用戶創(chuàng)建失敗,或者整個(gè)過程中出現(xiàn)異常,則返回狀態(tài)代碼 500 – 內(nèi)部服務(wù)器錯(cuò)誤,并返回相關(guān)錯(cuò)誤消息。

下面的清單顯示了我們?nèi)绾螌?shí)現(xiàn)這些任務(wù)。

清單 9.5 帳戶控制器.注冊方法

[HttpPost][ResponseCache(CacheProfileName = "NoCache")]public async Task<ActionResult> Register(RegisterDTO input){ try { if (ModelState.IsValid) ? { var newUser = new ApiUser(); newUser.UserName = input.UserName; newUser.Email = input.Email; var result = await _userManager.CreateAsync( newUser, input.Password); ? if (result.Succeeded) ? { _logger.LogInformation( "User {userName} ({email}) has been created.", newUser.UserName, newUser.Email); return StatusCode(201, $"User '{newUser.UserName}' has been created."); } else throw new Exception( string.Format("Error: {0}", string.Join(" ", result.Errors.Select(e => e.Description)))); } else { var details = new ValidationProblemDetails(ModelState); details.Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"; details.Status = StatusCodes.Status400BadRequest; return new BadRequestObjectResult(details); } } catch (Exception e) ? { var exceptionDetails = new ProblemDetails(); exceptionDetails.Detail = e.Message; exceptionDetails.Status = StatusCodes.Status500InternalServerError; exceptionDetails.Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1"; return StatusCode( StatusCodes.Status500InternalServerError, exceptionDetails); }}

? 檢查模型狀態(tài)并采取相應(yīng)措施

? 嘗試創(chuàng)建用戶

? 檢查結(jié)果并采取相應(yīng)措施

? 捕獲任何異常并返回錯(cuò)誤

這段代碼應(yīng)該不難理解。唯一的新東西是使用UserManager服務(wù)及其CreateAsync方法,該方法返回IdentityResult類型的對象,其中包含發(fā)生的結(jié)果或錯(cuò)誤?,F(xiàn)在我們有一個(gè) Register 方法,我們可以通過嘗試創(chuàng)建新用戶來測試它。

創(chuàng)建測試用戶

在調(diào)試模式下啟動項(xiàng)目,并像往常一樣等待 SwaggerUI 起始頁加載。然后,我們應(yīng)該看到一個(gè)新的 POST 帳戶/注冊端點(diǎn),我們可以擴(kuò)展它,如圖 9.5 所示。

使用 ASP.NET Core 構(gòu)建 Web API:9 身份驗(yàn)證和授權(quán)

圖 9.5 SwaggerUI 中的 /帳戶/注冊終結(jié)點(diǎn)

我們可以通過單擊右上角的“試用”按鈕來測試新方法。一旦我們這樣做,我們將能夠使用實(shí)際的用戶名、電子郵件和密碼值填充示例 JSON。讓我們使用以下值進(jìn)行第一次測試:

{ "userName": "TestUser", "email": "TestEmail", "password": "TestPassword"}

請求應(yīng)返回 HTTP 狀態(tài)代碼 400,并帶有解釋錯(cuò)誤原因(電子郵件格式無效)的響應(yīng)正文:

{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "Email": [ "The Email field is not a valid e-mail address." ] }}

此響應(yīng)表示模型狀態(tài)驗(yàn)證工作正常。目前為止,一切都好?,F(xiàn)在,讓我們修復(fù)電子郵件字段并使用以下值執(zhí)行新測試:

{ "userName": "TestUser", "email": "test-user@email.com", "password": "TestPassword"}

現(xiàn)在,請求應(yīng)返回 HTTP 狀態(tài)代碼 500,并帶有一個(gè)響應(yīng)正文,解釋新的錯(cuò)誤原因(密碼格式無效):

{ "type": "https://tools.ietf.org/html/rfc7231#section-6.6.1", "status": 500, "detail": "Error: Passwords must have at least one non alphanumeric? character. Passwords must have at least one digit ('0'-'9')."}

該錯(cuò)誤警告我們密碼不夠強(qiáng) – 再次確認(rèn)我們的驗(yàn)證檢查正在工作。現(xiàn)在我們可以修復(fù)最后一個(gè)問題,并使用以下值執(zhí)行第三個(gè)(理想情況下是最后一個(gè))測試:

{ "userName": "TestUser", "email": "test-user@email.com", "password": "MyVeryOwnTestPassword123$"}

我們應(yīng)該會收到一條確認(rèn)消息,指出用戶已創(chuàng)建。

注意隨意將示例中的用戶名和/或密碼替換為您自己的值。但請務(wù)必記下它們,尤其是密碼,因?yàn)?UserManager.CreateAsync 方法會將其作為不可逆的哈希值存儲在 [AspNetUsers] 中。密碼哈希]列。

現(xiàn)在我們完成了寄存器部分。讓我們繼續(xù)討論登錄方法。

實(shí)現(xiàn)登錄方法

我們的任務(wù)是創(chuàng)建一個(gè)合適的登錄DTO并使用它來實(shí)現(xiàn)登錄操作方法。讓我們從 LoginDTO 類開始,它(我們現(xiàn)在應(yīng)該知道)只需要兩個(gè)屬性:用戶名和密碼(請參閱下面的列表)。

清單 9.6 登錄DTO類

using System.ComponentModel.DataAnnotations; namespace MyBGList.DTO{ public class LoginDTO { [Required] [MaxLength(255)] public string? UserName { get; set; } [Required] public string? Password { get; set; } }}

現(xiàn)在我們可以實(shí)現(xiàn) AccountController.Login 方法,該方法需要處理以下任務(wù):

  1. 接受登錄DTO輸入。
  2. 檢查模型狀態(tài)以確保輸入有效;否則,返回記錄錯(cuò)誤的狀態(tài)代碼 400 – 錯(cuò)誤請求。
  3. 如果用戶存在且密碼匹配,請生成一個(gè)新令牌,并將其與狀態(tài)代碼 200 – 確定一起發(fā)送給用戶。
  4. 如果用戶不存在、密碼不匹配和/或在此過程中發(fā)生任何異常,請返回狀態(tài)代碼 401 – 未經(jīng)授權(quán),并返回相關(guān)錯(cuò)誤消息。

下面的清單包含這些任務(wù)的源代碼。

9.7 賬戶控制器的登錄方法

[HttpPost][ResponseCache(CacheProfileName = "NoCache")]public async Task<ActionResult> Login(LoginDTO input){ try { if (ModelState.IsValid) ? { var user = await _userManager.FindByNameAsync(input.UserName); if (user == null || !await _userManager.CheckPasswordAsync( user, input.Password)) throw new Exception("Invalid login attempt."); else { var signingCredentials = new SigningCredentials( ? new SymmetricSecurityKey( System.Text.Encoding.UTF8.GetBytes( _configuration["JWT:SigningKey"])), SecurityAlgorithms.HmacSha256); var claims = new List<Claim>(); ? claims.Add(new Claim( ClaimTypes.Name, user.UserName)); var jwtObject = new JwtSecurityToken( ? issuer: _configuration["JWT:Issuer"], audience: _configuration["JWT:Audience"], claims: claims, expires: DateTime.Now.AddSeconds(300), signingCredentials: signingCredentials); var jwtString = new JwtSecurityTokenHandler() ? .WriteToken(jwtObject); return StatusCode( ? StatusCodes.Status200OK, jwtString); } } else { var details = new ValidationProblemDetails(ModelState); details.Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"; details.Status = StatusCodes.Status400BadRequest; return new BadRequestObjectResult(details); } } catch (Exception e) ? { var exceptionDetails = new ProblemDetails(); exceptionDetails.Detail = e.Message; exceptionDetails.Status = StatusCodes.Status401Unauthorized; exceptionDetails.Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1"; return StatusCode( StatusCodes.Status401Unauthorized, exceptionDetails); }}

? 檢查模型狀態(tài)并采取相應(yīng)措施

? 生成簽名憑據(jù)

? 設(shè)置用戶聲明

? 實(shí)例化 JWT 對象實(shí)例

? 生成 JWT 加密字符串

? 將 JWT 返回給調(diào)用方

? 捕獲任何異常并返回錯(cuò)誤

同樣,此代碼應(yīng)該易于理解 – 除了 JWT 生成部分,它值得一些額外的解釋。該部分可以分為四個(gè)部分,我用空格分隔,每個(gè)部分設(shè)置一個(gè)變量,該變量在 JWT 創(chuàng)建過程中起著獨(dú)特的作用:

  • 簽名憑據(jù) – 此變量存儲使用 HMAC SHA-256 加密算法加密的 JWT 簽名。請注意,簽名密鑰是從配置設(shè)置中檢索的,與前面的程序.cs文件中的授權(quán)服務(wù)一樣。此方法可確保寫入和讀取過程將使用相同的值,這意味著簽名密鑰將匹配。
  • 聲明 – 此變量存儲我們要為其生成 JWT 的用戶的聲明列表。授權(quán)過程將使用這些聲明來檢查是否允許用戶訪問每個(gè)請求的資源(稍后會詳細(xì)介紹)。請注意,現(xiàn)在,我們正在設(shè)置一個(gè)與用戶的 UserName 屬性對應(yīng)的聲明。我們很快就會添加更多聲明。
  • jwtObject – 此變量通過將簽名憑據(jù)、聲明列表、配置文件檢索的頒發(fā)者和受眾值以及合適的過期時(shí)間(300 秒)放在一起來存儲 JWT 本身的實(shí)例(作為 C# 對象)。
  • jwtString – 此變量存儲 JWT 的加密字符串表示形式。此值是我們需要發(fā)送回客戶端的值,以便他們可以在后續(xù)請求的授權(quán)標(biāo)頭中設(shè)置它。

注意我們正在使用其他幾個(gè)UserManager方法:FindByNameAsync和CheckPasswordAsync。因?yàn)樗麄兊拿质遣谎宰悦鞯模岳斫馑麄冏鍪裁磻?yīng)該不難。

使用此方法,我們的帳戶控制器已準(zhǔn)備就緒,我們實(shí)現(xiàn)的身份驗(yàn)證部分也已準(zhǔn)備就緒。現(xiàn)在我們需要測試它。

對測試用戶進(jìn)行身份驗(yàn)證

要測試帳戶控制器的登錄方法,我們可以使用通過注冊方法創(chuàng)建的測試用戶。在調(diào)試模式下啟動項(xiàng)目,訪問 SwaggerUI 主儀表板,然后選擇新的 POST 帳戶/登錄端點(diǎn)(圖 9.6)。

使用 ASP.NET Core 構(gòu)建 Web API:9 身份驗(yàn)證和授權(quán)

圖 9.6 SwaggerUI 中的 /帳戶/登錄端點(diǎn)

單擊右上角的試用,并使用我們創(chuàng)建的測試用戶的用戶名和密碼值填充示例 JSON:

{ "userName": "TestUser", "password": " MyVeryOwnTestPassword123$"}

如果我們正確執(zhí)行了所有操作,我們應(yīng)該收到狀態(tài)代碼 200 – OK 響應(yīng),響應(yīng)正文中帶有 JWT(圖 9.7)。

使用 ASP.NET Core 構(gòu)建 Web API:9 身份驗(yàn)證和授權(quán)

圖 9.7 /帳戶/使用 JWT 的登錄響應(yīng)

現(xiàn)在,我們的 Web API 配備了有效的身份驗(yàn)證機(jī)制,包括通過 ASP.NET 核心身份處理的注冊和登錄過程。在下一節(jié)中,我們將基于它定義一些授權(quán)規(guī)則。

9.3 授權(quán)設(shè)置

在本部分中,我們將使用由帳戶控制器的登錄方法生成的 JWT 將我們的某些 API 終結(jié)點(diǎn)限制為授權(quán)用戶。為了獲得這個(gè)結(jié)果,我們需要注意兩個(gè)不同的方面:

  • 客戶端 – 添加包含 JWT 的授權(quán) HTTP 標(biāo)頭,以使用我們選擇的測試客戶端 (SwaggerUI) 正確模擬某些“授權(quán)”請求。
  • 服務(wù)器端 – 設(shè)置一些授權(quán)規(guī)則,使某些現(xiàn)有控制器(和最小 API)的操作方法僅供具有具有所需聲明的有效 JWT 的調(diào)用方訪問。

9.3.1 添加授權(quán) HTTP 標(biāo)頭

由于我們的帳戶控制器的登錄方法以純文本形式返回 JWT,因此我們可以做的最有效的事情是更新我們現(xiàn)有的 Swashbuckler SwaggerUI 配置,使其接受任意字符串(如果存在),該字符串將在執(zhí)行請求之前放入授權(quán) HTTP 標(biāo)頭中。與往常一樣,所需的更新將在程序.cs文件中執(zhí)行。

從客戶端處理授權(quán)標(biāo)頭

我們將要實(shí)現(xiàn)的技術(shù)旨在模擬實(shí)際的 REST 客戶端在執(zhí)行請求時(shí)會執(zhí)行的操作。JWT 不應(yīng)手動處理。大多數(shù)客戶端 JavaScript 框架(如 Angular 和 React)提供(或允許使用)HTTP 攔截器,這些攔截器可用于在調(diào)度之前將任意標(biāo)頭(例如帶有先前獲取的令牌的授權(quán)標(biāo)頭)附加到所有請求。

有關(guān) HTTP 攔截器的其他信息,請查看以下 URL:

  • 角度(內(nèi)置接口):https://angular.io/api/common/http/HttpInterceptor
  • Axios(用于 React 和其他框架):https://axios-http.com/docs/interceptors

我們需要添加新的安全定義,以告訴 Swagger 我們希望 API 的保護(hù)類型,以及全局強(qiáng)制實(shí)施的新安全要求。以下清單顯示了如何操作。

示例 9.8 程序.cs文件:Swagger的持有者令牌設(shè)置

using Microsoft.OpenApi.Models; ? // ... existing code builder.Services.AddSwaggerGen(options => { options.ParameterFilter<SortColumnFilter>(); options.ParameterFilter<SortOrderFilter>(); options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme ? { In = ParameterLocation.Header, Description = "Please enter token", Name = "Authorization", Type = SecuritySchemeType.Http, BearerFormat = "JWT", Scheme = "bearer" }); options.AddSecurityRequirement(new OpenApiSecurityRequirement ? { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type=ReferenceType.SecurityScheme, Id="Bearer" } }, Array.Empty<string>() } });});

? 必需的命名空間

? 新的招搖安全定義

? 新的招搖安全要求

由于此更新,帶有掛鎖圖標(biāo)的新授權(quán)按鈕將出現(xiàn)在 SwaggerUI 的右上角(圖 9.8)。如果我們單擊它,將出現(xiàn)一個(gè)彈出窗口,讓我們有機(jī)會插入要在授權(quán) HTTP 標(biāo)頭中使用的持有者令牌。

使用 ASP.NET Core 構(gòu)建 Web API:9 身份驗(yàn)證和授權(quán)

圖9.8 SwaggerUI授權(quán)按鈕和彈窗

這正是我們需要將 JWT 添加到我們的請求中的內(nèi)容?,F(xiàn)在,作業(yè)的客戶端部分已經(jīng)完成,我們可以切換到服務(wù)器端。

9.3.2 設(shè)置 [授權(quán)] 屬性

我們必須選擇哪些 API 端點(diǎn)應(yīng)該對所有人可用(因?yàn)樗鼈円呀?jīng)可用),以及限制、限制或阻止哪些端點(diǎn)。典型的方法是允許對只讀終結(jié)點(diǎn)進(jìn)行公共/匿名訪問,這些終結(jié)點(diǎn)不泄露保留數(shù)據(jù),并將其他所有內(nèi)容限制為經(jīng)過身份驗(yàn)證(和授權(quán))的用戶。讓我們使用通用邏輯,根據(jù)產(chǎn)品所有者的顯式請求設(shè)計(jì)給定的實(shí)現(xiàn)方案。假設(shè)我們希望保持對所有操作方法的無限制訪問,但以下方法除外:

  • 棋盤游戲控制器 – 發(fā)布、刪除
  • 域控制器 – 發(fā)布、刪除
  • 機(jī)械控制器 – 發(fā)布、刪除
  • 種子控制器 – 放置

我們可以很容易地看到,所有這些方法都是為了將永久更改應(yīng)用于我們的數(shù)據(jù)庫,因此將它們放在授權(quán)規(guī)則后面很有意義。我們絕對不希望某些匿名用戶刪除、更新或以其他方式更改我們的棋盤游戲數(shù)據(jù)!

為了將我們的計(jì)劃付諸實(shí)踐,我們可以用 [Authorize] 屬性來修飾這些方法,該屬性是 Microsoft.AspNetCore.Authorization 命名空間的一部分。此屬性可應(yīng)用于控制器、操作方法和最小 API 方法,以根據(jù)身份驗(yàn)證方案、策略和/或角色設(shè)置特定的授權(quán)規(guī)則??梢允褂脤傩缘膮?shù)配置這些規(guī)則(我們將在稍后看到)。在沒有參數(shù)的情況下使用時(shí),[Authorize] 屬性以其最基本的形式將限制對經(jīng)過身份驗(yàn)證的用戶的訪問,無論其權(quán)限如何。

由于我們尚未為用戶定義任何策略或角色,因此可以使用屬性的無參數(shù)行為開始實(shí)現(xiàn)過程。打開以下控制器:BoardGamesController、DomainsController、MechanicsController和SeedController。然后將 [Authorize] 屬性添加到其發(fā)布、刪除和放置方法中,如下所示:

using Microsoft.AspNetCore.Authorization; ? // ... existing code [Authorize] ? [HttpPost(Name = "UpdateBoardGame")] [ResponseCache(CacheProfileName = "NoCache")] public async Task<RestDTO<BoardGame?>> Post(BoardGameDTO model)

? 必需的命名空間

? 授權(quán)屬性

現(xiàn)在,我們所有可能更改數(shù)據(jù)的操作方法將只有經(jīng)過身份驗(yàn)證的用戶才能訪問。

選擇默認(rèn)訪問行為

請務(wù)必了解,通過將 [Authorize] 屬性應(yīng)用于某些特定操作方法,我們將為未經(jīng)授權(quán)的用戶隱式設(shè)置默認(rèn)允許、選擇阻止邏輯。換句話說,我們的意思是,除了那些受 [Authorize] 屬性限制的操作方法之外,所有操作方法都允許匿名訪問。我們可以將此邏輯反轉(zhuǎn)為默認(rèn)阻止,選擇允許,方法是將 [Authorize] 屬性設(shè)置為整個(gè)控制器,然后有選擇地將 [AllowAnonymous] 屬性用于我們希望每個(gè)人都可以訪問的操作方法。

這兩種行為都是可行的,具體取決于特定的用例。一般而言,限制性更強(qiáng)的方法(默認(rèn)阻止,選擇允許)被認(rèn)為更安全,不易發(fā)生人為(開發(fā)人員)錯(cuò)誤。通常,忘記 [Authorize] 屬性比忘記 [AllowAnonymous] 屬性更糟糕,因?yàn)樗苋菀讓?dǎo)致數(shù)據(jù)泄露。

在我們的示例方案中,保護(hù)單個(gè)操作方法可能是可以接受的,至少對于那些具有混合訪問行為(匿名和受限操作方法)的控制器。例外情況是種子控制器,它旨在僅托管受限制的操作方法。在這種情況下,在控制器級別設(shè)置 [Authorize] 屬性會更合適。讓我們在繼續(xù)之前這樣做。打開 SeedController.cs 文件,并將 [Authorize] 屬性從操作方法移動到控制器:

[Authorize][Route("[controller]")][ApiController]public class SeedController : ControllerBase

由于此更新,我們將添加到此控制器的所有操作方法將自動限制為授權(quán)用戶。我們不必記住做任何其他事情。

啟用最小 API 授權(quán)

在進(jìn)入測試階段之前,我們應(yīng)該看看如何在最小 API 中使用 [Authorize] 屬性,而無需額外的努力。讓我們添加一個(gè)最小 API 方法來處理新的 /auth/test/1 終結(jié)點(diǎn),該終結(jié)點(diǎn)僅供授權(quán)用戶訪問。下面的清單包含源代碼。

示例 9.9 程序.cs文件:/auth/test/1 最小 API 端點(diǎn)

using Microsoft.AspNetCore.Authorization; // ... existing code app.MapGet("/auth/test/1", [Authorize] [EnableCors("AnyOrigin")] [ResponseCache(NoStore = true)] () => { return Results.Ok("You are authorized!"); });

現(xiàn)在,我們終于準(zhǔn)備好測試到目前為止所做的工作了。

9.3.3 測試授權(quán)流程

在調(diào)試模式下啟動項(xiàng)目,然后訪問 SwaggerUI 儀表板。與往常一樣,此客戶端是我們將用來執(zhí)行測試的客戶端。

我們應(yīng)該做的第一件事是檢查授權(quán)限制是否正常工作。我們添加的 /auth/test/1 最小 API 端點(diǎn)是該任務(wù)的完美候選項(xiàng),因?yàn)樗梢酝ㄟ^不會影響我們數(shù)據(jù)的簡單 GET 請求來調(diào)用。我們將使用 SwaggerUI 調(diào)用該終結(jié)點(diǎn),并確保它返回狀態(tài)代碼 401 – 未經(jīng)授權(quán)的響應(yīng),如圖 9.9 所示。

使用 ASP.NET Core 構(gòu)建 Web API:9 身份驗(yàn)證和授權(quán)

圖 9.9 /auth/test/1 端點(diǎn)返回狀態(tài)代碼 401 – 未授權(quán)

目前為止,一切都好。對于具有 [Authorize] 屬性的任何方法,預(yù)計(jì)會出現(xiàn)未經(jīng)授權(quán)的響應(yīng),因?yàn)槲覀兩形赐ㄟ^身份驗(yàn)證。讓我們填補(bǔ)這個(gè)空白,再試一次。執(zhí)行以下步驟:

  1. 使用測試用戶的用戶名和密碼值調(diào)用帳戶/登錄終結(jié)點(diǎn),就像我們之前測試它時(shí)所做的那樣,以接收有效的 JWT。
  2. 通過選擇它并按 Ctrl C 將 JWT 復(fù)制到剪貼板。
  3. 單擊我們添加的授權(quán)按鈕以顯示彈出窗口。
  4. 將 JWT 粘貼到彈出窗口的輸入文本框中,然后單擊授權(quán)以針對下一個(gè)請求進(jìn)行設(shè)置。

現(xiàn)在我們可以調(diào)用 /auth/test/1 端點(diǎn),看看持有者令牌是否允許我們執(zhí)行此請求。如果一切順利(并且我們在令牌過期之前的 300 秒內(nèi)執(zhí)行測試),我們應(yīng)該會在響應(yīng)正文中看到 200 – OK 狀態(tài)代碼和“您已 獲得授權(quán)!”消息,如圖 9.10 所示。

使用 ASP.NET Core 構(gòu)建 Web API:9 身份驗(yàn)證和授權(quán)

圖 9.10 /auth/test/1 端點(diǎn)返回狀態(tài)代碼 200 – 正常

此結(jié)果表明我們基于 JWT 的身份驗(yàn)證和授權(quán)流正在工作;我們已成功將某些方法限制為授權(quán)用戶。但是,我們的授權(quán)規(guī)則仍然是基本的。我們只能區(qū)分匿名用戶和經(jīng)過身份驗(yàn)證的用戶,考慮到后者無需進(jìn)一步檢查即可獲得授權(quán)。理想情況下,我們應(yīng)該為我們的 Web API 提供一個(gè)更精細(xì)的訪問控制系統(tǒng),允許我們設(shè)置其他授權(quán)行為,例如僅授權(quán)某些用戶執(zhí)行某些操作。在下一部分中,我們將通過實(shí)現(xiàn)基于角色的聲明機(jī)制來實(shí)現(xiàn)這一點(diǎn)。

9.4 基于角色的訪問控制

假設(shè)我們要?jiǎng)?chuàng)建不同的 ACL 來支持以下經(jīng)過身份驗(yàn)證的用戶類型:

  • 基本用戶 – 應(yīng)允許他們訪問只讀終端節(jié)點(diǎn),而不能訪問其他任何內(nèi)容,例如匿名(未注冊)用戶。
  • 審閱人 – 他們應(yīng)有權(quán)訪問只讀端點(diǎn)和更新端點(diǎn),但不能刪除任何內(nèi)容或?yàn)閿?shù)據(jù)庫設(shè)定種子。
  • 管理員 – 他們應(yīng)該能夠執(zhí)行任何操作(讀取、更新、刪除和播種)。

現(xiàn)在的情況是,所有經(jīng)過身份驗(yàn)證的用戶都被視為管理員:他們可以執(zhí)行任何操作,因?yàn)?[Authorize] 屬性僅檢查該基本狀態(tài)。要改變這種行為,我們需要找到一種方法將這些用戶組織到不同的組中,并為每個(gè)組分配特定的權(quán)限。在 ASP.NET Core中,我們可以通過使用角色來實(shí)現(xiàn)此結(jié)果。

基于角色的訪問控制RBAC) 是一種內(nèi)置的授權(quán)策略,它提供了一種為不同用戶分配不同權(quán)限的便捷方法。每個(gè)角色的行為都像一個(gè)組,因此我們可以向其添加用戶并為其設(shè)置特定的授權(quán)規(guī)則。定義規(guī)則后,它們將應(yīng)用于具有該特定角色的所有用戶。

實(shí)施 RBAC 策略是處理任務(wù)的好方法,因?yàn)樗试S我們對用戶進(jìn)行分類。簡而言之,這是我們需要做的:

  • 注冊其他用戶。我們至少需要其中的兩個(gè):TestModerator 和 TestAdministrator,每個(gè)都代表我們想要支持的用戶類型。
  • 創(chuàng)建一組預(yù)定義的角色。根據(jù)我們的要求,我們需要其中兩個(gè):版主和管理員。我們不需要為基本用戶添加角色,因?yàn)樗麄儜?yīng)具有與匿名用戶相同的權(quán)限,并且無參數(shù) [Authorize] 屬性已經(jīng)處理了這些權(quán)限。
  • 將用戶添加到角色。具體來說,我們需要將測試管理員用戶分配給審閱人角色,將測試管理員用戶分配給管理員角色
  • 將基于角色的聲明添加到 JWT。由于 JWT 包含經(jīng)過身份驗(yàn)證的用戶聲明的集合,因此我們需要將用戶的角色放在這些聲明中,以便 ASP.NET Core 授權(quán)中間件能夠確認(rèn)這些聲明并采取相應(yīng)的操作。
  • 設(shè)置基于角色的授權(quán)規(guī)則。我們可以通過更新要限制為版主和管理員的操作方法中的 [Authorize] 屬性來執(zhí)行此任務(wù),以便他們要求 JWT 中存在相應(yīng)的與角色相關(guān)的聲明。

9.4.1 注冊新用戶

要做的第一件事應(yīng)該很容易完成,因?yàn)槲覀冊谕ㄟ^創(chuàng)建 TestUser 帳戶測試帳戶/注冊終結(jié)點(diǎn)時(shí)就這樣做了。我們必須執(zhí)行相同的端點(diǎn)兩次才能再添加兩個(gè)用戶。以下是我們可用于創(chuàng)建測試審查器帳戶的 JSON 值:

{ "userName": "TestModerator", "email": "test-moderator@email.com", "password": "MyVeryOwnTestPassword123$"}

以下是測試管理員帳戶的值:

{ "userName": "TestAdministrator", "email": "test-administrator@email.com", "password": "MyVeryOwnTestPassword123$"}

注意與往常一樣,請隨時(shí)更改用戶名和/或密碼。

用戶就是這樣。讓我們繼續(xù)處理角色。

9.4.2 創(chuàng)建新角色

創(chuàng)建角色的最方便方法是使用 RoleManager API,它是 Microsoft.AspNetCore.Identity 命名空間的一部分。我們將使用其 CreateAsync 方法,該方法接受 IdentityRole 對象作為參數(shù),并使用它來在持久性存儲(在我們的方案中為 [AspNetRoles] 數(shù)據(jù)庫表)中創(chuàng)建新記錄,為其分配唯一 ID。以下是我們?nèi)绾螌?shí)現(xiàn)它:

await _roleManager.CreateAsync(new IdentityRole("RoleName"));

如我們所見,IdentityRole 對象的構(gòu)造函數(shù)接受表示角色名稱的字符串類型的值。創(chuàng)建角色后,我們需要在代碼中使用此名稱來引用它。因此,將這些名稱定義為常量可能是一個(gè)好主意。

添加角色名稱常量

若要將這些名稱定義為常量,請?jiān)?/Constants/ 文件夾中創(chuàng)建一個(gè)新的 RoleNames.cs 文件,并使用以下清單中的代碼填充該文件。

清單 9.10 /常量/角色名稱.cs文件

namespace MyBGList.Constants{ public static class RoleNames { public const string Moderator = "Moderator"; public const string Administrator = "Administrator"; }}

這些常量將允許我們每次都使用強(qiáng)類型方法而不是文字字符串來引用我們的角色,從而防止人為錯(cuò)誤。現(xiàn)在,我們可以編寫代碼來創(chuàng)建這些角色。因?yàn)槲覀冋務(wù)摰氖且粋€(gè)可能只執(zhí)行一次的數(shù)據(jù)庫種子任務(wù),所以最好的地方是放在我們的 SeedController 中。但是使用現(xiàn)有的 Put 方法(我們在第 5 章中實(shí)現(xiàn)了該方法,將棋盤游戲數(shù)據(jù)插入數(shù)據(jù)庫)將是一種不好的做法,因?yàn)樗鼤茐膯我回?zé)任原則。相反,我們應(yīng)該重構(gòu) SeedController 為我們希望它處理的兩個(gè)任務(wù)創(chuàng)建不同的端點(diǎn)(和操作方法)。我們將重命名現(xiàn)有終結(jié)點(diǎn) /Seed/BoardGameData,并為新的種子任務(wù)創(chuàng)建新的 /Seed/ AuthData。

重構(gòu)種子控制器

若要重構(gòu)種子控制器,請打開 /Controllers/SeedController.cs 文件,然后修改現(xiàn)有代碼,如以下清單所示(更新的行以粗體顯示)。

清單 9.11 /控制器/種子控制器.cs 文件:類重構(gòu)

using Microsoft.AspNetCore.Authorization; ?using Microsoft.AspNetCore.Identity; ? // ... existing codenamespace MyBGList.Controllers{ [Authorize] [Route("[controller]/[action]")] ? [ApiController] public class SeedController : ControllerBase { // ... existing code private readonly RoleManager<IdentityRole> _roleManager; ? private readonly UserManager<ApiUser> _userManager; ? public SeedController( ApplicationDbContext context, IWebHostEnvironment env, ILogger<SeedController> logger, RoleManager<IdentityRole> roleManager, ? UserManager<ApiUser> userManager) ? { _context = context; _env = env; _logger = logger; _roleManager = roleManager; ? _userManager = userManager; ? } [HttpPut] ? [ResponseCache(CacheProfileName = "NoCache")] public async Task<IActionResult> BoardGameData() ? { // ... existing code } [HttpPost] [ResponseCache(NoStore = true)] public async Task<IActionResult> AuthData() ? { throw new NotImplementedException(); } }}

? 必需的命名空間

? 新的基于屬性的路由行為

? 角色管理器接口

? 用戶管理器接口

? 現(xiàn)有看跌期權(quán)操作方法更名為棋盤游戲數(shù)據(jù)

? 新的身份驗(yàn)證數(shù)據(jù)操作方法

此代碼應(yīng)該易于理解。我們更改了控制器的路由規(guī)則,使端點(diǎn)與操作名稱匹配;然后,我們注入了創(chuàng)建和分配角色所需的角色管理器和用戶管理器 API。最后,我們重命名了現(xiàn)有的 Put 操作方法 BoardGameData,并添加了一個(gè)新的 AuthData 操作方法來處理角色創(chuàng)建任務(wù)。

請注意,我們沒有實(shí)現(xiàn)新方法,而是專注于 SeedController 的重構(gòu)部分?,F(xiàn)在我們可以繼續(xù)實(shí)現(xiàn) AuthData 操作方法,將“未實(shí)現(xiàn)”代碼替換為以下列表中的代碼。

示例 9.12 /Controllers/SeedController.cs 文件: AuthData 方法

[HttpPost][ResponseCache(NoStore = true)]public async Task<IActionResult> AuthData(){ int rolesCreated = 0; int usersAddedToRoles = 0; if (!await _roleManager.RoleExistsAsync(RoleNames.Moderator)) { await _roleManager.CreateAsync( new IdentityRole(RoleNames.Moderator)); ? rolesCreated ; } if (!await _roleManager.RoleExistsAsync(RoleNames.Administrator)) { await _roleManager.CreateAsync( new IdentityRole(RoleNames.Administrator)); ? rolesCreated ; } var testModerator = await _userManager .FindByNameAsync("TestModerator"); if (testModerator != null && !await _userManager.IsInRoleAsync( testModerator, RoleNames.Moderator)) { await _userManager.AddToRoleAsync(testModerator,? RoleNames.Moderator); ? usersAddedToRoles ; } var testAdministrator = await _userManager .FindByNameAsync("TestAdministrator"); if (testAdministrator != null && !await _userManager.IsInRoleAsync( testAdministrator, RoleNames.Administrator)) { await _userManager.AddToRoleAsync( testAdministrator, RoleNames.Moderator); ? await _userManager.AddToRoleAsync( testAdministrator, RoleNames.Administrator); ? usersAddedToRoles ; } return new JsonResult(new { RolesCreated = rolesCreated, UsersAddedToRoles = usersAddedToRoles });}

? 創(chuàng)建角色

? 將用戶添加到角色

正如我們所看到的,我們包括了一些檢查以確保

  • 僅當(dāng)角色尚不存在時(shí),才會創(chuàng)建角色。
  • 僅當(dāng)用戶存在且尚未加入時(shí),才會將用戶添加到角色中。

如果多次調(diào)用操作方法,這些控件將防止代碼引發(fā)錯(cuò)誤。

提示Test管理員用戶已添加到多個(gè)角色:審閱者和管理員。這對于我們的任務(wù)來說完全沒問題,因?yàn)槲覀兿M芾韱T擁有與版主相同的權(quán)限。

9.4.3 為用戶分配角色

由于我們已經(jīng)創(chuàng)建了 TestAdministratorator 和 TestAdministrator 用戶,因此將他們分配給新角色將是一項(xiàng)簡單的任務(wù)。我們需要在調(diào)試模式下啟動我們的項(xiàng)目,訪問 SwaggerUI,并執(zhí)行 /Seed/AuthData 端點(diǎn)。

由于我們之前將 [Authorize] 屬性設(shè)置為整個(gè) SeedController,但是,如果我們嘗試在沒有有效 JWT 的情況下調(diào)用該終結(jié)點(diǎn),我們將收到 401 – 未授權(quán)狀態(tài)代碼。為了避免這種結(jié)果,我們有兩個(gè)選擇:

  • 使用 /Account/Login 端點(diǎn)對自己進(jìn)行身份驗(yàn)證(任何用戶都可以做到這一點(diǎn)),然后在 SwaggerUI 的授權(quán)彈出窗口中設(shè)置生成的 JWT。
  • 在執(zhí)行項(xiàng)目并調(diào)用 /Seed/AuthData 終結(jié)點(diǎn)之前,請注釋掉 [Authorize] 屬性,然后取消注釋它。

無論我們采用哪種路線,假設(shè)一切順利,我們都應(yīng)該收到帶有以下 JSON 響應(yīng)正文的 200 – OK 狀態(tài)代碼:

{ "rolesCreated": 2, "usersAddedToRoles": 2}

我們已經(jīng)成功創(chuàng)建了我們的角色,并將我們的用戶添加到其中?,F(xiàn)在,我們需要確保將這些角色放在持有者令牌中,以便授權(quán)中間件可以檢查它們的存在并采取相應(yīng)的行動。

9.4.4 向 JWT 添加基于角色的聲明

若要將角色添加到持有者令牌,我們需要打開 /Controllers/AccountController.cs 文件,然后更新 Login 操作方法,為成功進(jìn)行身份驗(yàn)證的用戶所屬的每個(gè)角色添加一個(gè)聲明。以下代碼片段演示了如何(粗體換行):

// ... existing code var claims = new List<Claim>();claims.Add(new Claim( ClaimTypes.Name, user.UserName));claims.AddRange( (await _userManager.GetRolesAsync(user)) .Select(r => new Claim(ClaimTypes.Role, r))); // ... existing code

如我們所見,經(jīng)過身份驗(yàn)證的用戶的 JWT 現(xiàn)在包含零個(gè)、一個(gè)或多個(gè)基于角色的聲明,具體取決于用戶所屬的角色數(shù)量。這些聲明將用于是否授權(quán)該用戶的請求,具體取決于我們?nèi)绾螢槊總€(gè)控制器和/或操作方法配置授權(quán)規(guī)則。

9.4.5 設(shè)置基于角色的身份驗(yàn)證規(guī)則

現(xiàn)在,我們已確保經(jīng)過身份驗(yàn)證的用戶的 JWT 將包含其每個(gè)角色(如果有)的聲明,我們可以更新現(xiàn)有的 [Authorize] 屬性以考慮角色。讓我們從主持人角色開始。打開 BoardGamesController、DomainsController 和 MechanicsController 文件,并按以下方式更改應(yīng)用于其更新方法的現(xiàn)有 [Authorize] 屬性:

[Authorize(Roles = RoleNames.Moderator)]

此代碼將更改屬性的行為?,F(xiàn)在,該屬性將僅授權(quán)具有審閱人角色的用戶,而不是授權(quán)所有經(jīng)過身份驗(yàn)證的用戶,而不管其角色如何。由于我們要添加對 RoleNames 靜態(tài)類的引用,因此還需要在每個(gè)控制器文件的頂部添加以下命名空間引用:

using MyBGList.Constants;

讓我們對管理員角色重復(fù)此過程。通過以下方式更改應(yīng)用于控制器刪除方法的現(xiàn)有 [Authorize] 屬性:

[Authorize(Roles = RoleNames.Administrator)]

然后打開 SeedController,并使用前面的屬性更新其 [Authorize] 屬性(我們將其應(yīng)用于控制器本身),因?yàn)閷⒃摽刂破飨拗茷楣芾韱T是我們分配的一部分。

9.4.6 測試 RBAC 流

為了執(zhí)行無害測試,我們可以使用要檢查的授權(quán)規(guī)則創(chuàng)建兩個(gè)新的最小 API 測試方法,而不是使用現(xiàn)有的端點(diǎn),這會對我們的數(shù)據(jù)進(jìn)行一些永久性更改。打開 Program.cs 文件,并在處理我們之前添加的 /auth/test/2 終結(jié)點(diǎn)的方法的正下方添加以下代碼:

app.MapGet("/auth/test/2", [Authorize(Roles = RoleNames.Moderator)] [EnableCors("AnyOrigin")] [ResponseCache(NoStore = true)] () => { return Results.Ok("You are authorized!"); }); app.MapGet("/auth/test/3", [Authorize(Roles = RoleNames.Administrator)] [EnableCors("AnyOrigin")] [ResponseCache(NoStore = true)] () => { return Results.Ok("You are authorized!"); });

現(xiàn)在,我們可以執(zhí)行以下測試周期,這與我們?yōu)榈谝粋€(gè)授權(quán)流設(shè)計(jì)的測試周期非常相似。在 SwaggerUI 主儀表板中,執(zhí)行以下步驟:

  1. 使用 TestUser 的用戶名和密碼調(diào)用帳戶/登錄端點(diǎn)以接收有效的 JWT。
  2. 此用戶不屬于任何角色。
  3. 將 JWT 復(fù)制到剪貼板,單擊 SwaggerUI 的授權(quán)按鈕,將其值復(fù)制到彈窗中的輸入文本框中,然后單擊授權(quán)按鈕關(guān)閉彈出窗口。
  4. 調(diào)用 /auth/test/2 和 /auth/test/3 終結(jié)點(diǎn)。
  5. 如果一切按預(yù)期工作,我們應(yīng)該得到一個(gè) 401 – 未經(jīng)授權(quán)的狀態(tài)代碼,因?yàn)檫@些端點(diǎn)僅限于版主和管理員,而 TestUser 不是其中之一,因?yàn)樗鼪]有相應(yīng)的角色。
  6. 對測試審查器帳戶重復(fù)步驟 1、2 和 3。
  7. 這一次,我們應(yīng)該收到 /auth/test/200 終結(jié)點(diǎn)的 2 – OK 狀態(tài)代碼和 /auth/test/401 終結(jié)點(diǎn)的 3 – 未授權(quán)狀態(tài)代碼。前者僅限于版主(我們是),后者僅適用于管理員(我們不是)。
  8. 對 TestAdministrator 帳戶重復(fù)步驟 1、2 和 3。
  9. 這一次,我們應(yīng)該為兩個(gè)終結(jié)點(diǎn)獲取 200 – OK 狀態(tài)代碼,因?yàn)樵搸魧儆趯忛喺吆凸芾韱T角色。

9.4.7 使用其他授權(quán)方法

正如我們在處理它時(shí)所看到的,我們實(shí)現(xiàn)的 RBAC 方法依賴于角色類型聲明 (ClaimTypes.Role) 來執(zhí)行其授權(quán)檢查。如果 JWT 令牌包含此類聲明,并且聲明的內(nèi)容與 [Authorize] 屬性的要求匹配,則用戶已獲得授權(quán)。

但是,可以分配和檢查許多其他聲明類型,以確定用戶是否獲得授權(quán)。我們只能授權(quán)擁有手機(jī)號碼的用戶,例如,通過使用 ClaimTypes.MobilePhone,我們可以執(zhí)行這樣的檢查,而不是用戶給定的角色,或者除了用戶給定的角色之外。

基于聲明的訪問控制

此方法稱為基于聲明的訪問控制CBAC),包括 RBAC 提供的相同功能以及更多功能,因?yàn)樗捎糜谕瑫r(shí)檢查任何聲明(或聲明集)。

注意我們可以說,RBAC 只不過是基于 ClaimTypes.Role 的單個(gè)特定聲明的 CBAC 的高級抽象。

與 RBAC 不同,RBAC 由于 [Authorize] 屬性的 Role 屬性而可以輕松快速地實(shí)現(xiàn),聲明要求是基于策略的,因此必須通過在 Program.cs 文件中定義和注冊策略來顯式聲明它們。下面介紹了如何添加“主持人使用移動電話”策略,該策略將檢查主持人角色和移動電話號碼是否存在:

builder.Services.AddAuthorization(options => { options.AddPolicy("ModeratorWithMobilePhone", policy => policy .RequireClaim(ClaimTypes.Role, RoleNames.Moderator) ? .RequireClaim(ClaimTypes.MobilePhone)); ?});

? 檢查具有給定值的索賠

? 僅檢查聲明是否存在

注意此技術(shù)與我們在第 3 章中用于注冊 CORS 策略的技術(shù)大致相同。

將上述代碼片段粘貼到 builder.service AddAuthentication 行下方的程序.cs文件中,以配置身份驗(yàn)證服務(wù)。然后我們可以通過以下方式將策略設(shè)置為 [Authorize] 屬性的參數(shù):

[Authorize(Policy = "ModeratorWithMobilePhone")]

此策略需要對現(xiàn)有代碼的以下部分進(jìn)行一些修改:

  • RegisterDTO 類,允許注冊用戶添加其手機(jī)號碼
  • 帳戶控制器的注冊操作方法,用于將移動電話值保存在數(shù)據(jù)庫中(如果存在)
  • 帳戶控制器的登錄操作方法,用于有條件地將 ClaimTypes.MobilePhone 的聲明添加到包含用戶移動電話號碼(如果存在)的 JWT 令牌

我不打算在本書中使用這種方法。我簡要展示它只是因?yàn)樗鼘τ趯?shí)現(xiàn)某些特定的授權(quán)要求很有用。

基于策略的訪問控制

盡管CBAC比RBAC更通用,但它允許我們僅檢查是否存在多個(gè)聲明中的一個(gè)和/或其特定值。如果聲明值不是單個(gè)值,或者我們需要更復(fù)雜的檢查,該怎么辦?我們可能希望定義一個(gè)策略,以僅授權(quán)年齡等于或大于 18 歲的用戶,并且我們無法通過檢查是否存在 ClaimTypes.DateOfBirth 聲明或特定出生日期值來執(zhí)行此操作。

每當(dāng)我們需要執(zhí)行此類檢查時(shí),我們都可以使用基于策略的訪問控制PBAC) 方法,這是 Microsoft.AspNetCore.Authorization 命名空間提供的最復(fù)雜和最通用的授權(quán)方法。此技術(shù)類似于 CBAC,因?yàn)樗€需要聲明性方法,即在 Program.cs 文件中聲明策略。但是,它不是僅僅檢查一個(gè)或多個(gè)聲明是否存在(以及可選的值),而是使用由一個(gè)或多個(gè)需求(IAuthorizationRequire)和需求處理程序(IAuthorizationHandler)組成的更通用的接口。

注意此接口也由 CBAC 的 RequireClaim 方法在后臺使用。我們可以說,RBAC 和 CBAC 都是基于預(yù)配置策略的 PBAC 的簡化實(shí)現(xiàn)。

我不打算在本書中使用 PBAC,因?yàn)樗枰獙?shí)現(xiàn)一些示例需求和需求處理程序類。但我將簡要介紹 RequireAssertion 方法,這是一種使用匿名函數(shù)配置和構(gòu)建基于策略的授權(quán)檢查的便捷方法。以下是我們?nèi)绾问褂么朔椒ǘx“等于或大于 18”策略的方法:

options.AddPolicy("MinAge18", policy => policy .RequireAssertion(ctx => ctx.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth) && DateTime.ParseExact( "yyyyMMdd", ctx.User.Claims.First(c => c.Type == ClaimTypes.DateOfBirth).Value, System.Globalization.CultureInfo.InvariantCulture) >= DateTime.Now.AddYears(-18)));

添加的值是由 RequireAssertion 方法公開的 AuthorizationHandlerContext 對象,其中包含對表示當(dāng)前用戶的 ClaimsPrincipal 的引用。ClaimsPrincipal 類不僅可用于檢查任何聲明的存在和/或值,還可用于使用、轉(zhuǎn)換和/或轉(zhuǎn)換這些值以滿足我們的所有需求。同樣,此策略可用作某些假設(shè) [Authorize] 屬性的參數(shù),以通過以下方式將某些控制器、操作方法和/或最小 API 方法限制為 18 歲以上的用戶:

[Authorize(Policy = "MinAge18")]

此策略還需要對我們現(xiàn)有的代碼進(jìn)行大量重構(gòu),因?yàn)槲覀兡壳安辉儐枺ú⑹占┳杂脩舻某錾掌凇3鲇谶@個(gè)原因,我將在這里停止,將前面的代碼僅供參考。

一些有用的授權(quán)相關(guān)參考

有關(guān) [授權(quán)] 屬性及其使用方法的其他信息,請參閱 http://mng.bz/Nmj2 中的指南。

要了解有關(guān) RBAC、CBAC 和 PBAC 的更多信息,請查看以下指南:

  • http://mng.bz/DZj9
  • http://mng.bz/lJnM
  • http://mng.bz/Bl8g

本節(jié)結(jié)束了我們的測試運(yùn)行,以及我們進(jìn)入 ASP.NET 核心身份驗(yàn)證和授權(quán)的旅程。重要的是要明白,我們只是觸及了這些龐大而復(fù)雜的主題的表面。我們在本章中整理的示例源代碼對于一些沒有敏感或有價(jià)值數(shù)據(jù)的基本 Web API 來說可能已經(jīng)足夠了,但除非我們使用一些額外的安全措施來支持它,否則它可能不適合更復(fù)雜的方案。

至于 ASP.NET 核心身份,我們只是觸及了框架可以做什么的表面,從PBAC到非JWT承載者,更不用說與第三方授權(quán)提供程序和協(xié)議(如OAuth2)的內(nèi)置集成,由于空間原因,我沒有處理。盡管如此,本章提供的廣泛概述仍應(yīng)有助于我們了解 Core 身份驗(yàn)證和授權(quán)的工作原理 ASP.NET 以及如何在典型的 Web API 方案中實(shí)現(xiàn)它們。

9.5 習(xí)題

將我們在本章中學(xué)到的知識印記下來的最好方法是用一些與標(biāo)識相關(guān)的升級任務(wù)來挑戰(zhàn)自己,我們的產(chǎn)品所有者可能希望分配給我們。與往常一樣,練習(xí)的解決方案可以在GitHub上的/Chapter_09/Exercises/文件夾中找到。若要測試它們,請將 MyBGList 項(xiàng)目中的相關(guān)文件替換為該文件夾中的文件,然后運(yùn)行應(yīng)用。

9.5.1 添加新角色

使用強(qiáng)類型方法將新的“SuperAdmin”角色添加到我們用于定義角色名稱的靜態(tài)類中。然后修改種子控制器的 AuthData 方法,以確保將創(chuàng)建新角色(如果該角色尚不存在)。

9.5.2 創(chuàng)建新用戶

使用帳戶/注冊端點(diǎn)創(chuàng)建新的“TestSuperAdmin”用戶,就像我們對 TestUser、TestAdministratoror 和 TestAdministrator 用戶所做的那樣。隨意選擇您自己的密碼(但請確保您會記住它以備將來使用)。

9.5.3 為用戶分配角色

修改 SeedController 的 AuthData 方法,將審閱人、管理員和超級管理員角色分配給測試超級管理員用戶。

9.5.4 實(shí)現(xiàn)測試端點(diǎn)

使用最小 API 添加新的 /auth/test/4 端點(diǎn),并將其訪問權(quán)限限制為具有超級管理員角色的授權(quán)用戶。

9.5.5 測試 RBAC 流

使用帳戶/登錄終結(jié)點(diǎn)恢復(fù) TestSuperAdmin 帳戶的 JWT,并使用它來嘗試訪問 /auth/test/4 終結(jié)點(diǎn),并確保新用戶和角色按預(yù)期工作。

總結(jié)

  • 身份驗(yàn)證是一種驗(yàn)證實(shí)體(或個(gè)人)是否是它(或他們)聲稱的機(jī)制。授權(quán)定義了實(shí)體(或個(gè)人)能夠做什么。
    • 這兩個(gè)進(jìn)程在任何需要限制對內(nèi)容、數(shù)據(jù)和/或終結(jié)點(diǎn)的訪問的 Web 應(yīng)用或服務(wù)中都起著關(guān)鍵作用。
  • 在大多數(shù)實(shí)現(xiàn)方法中,身份驗(yàn)證過程通常在授權(quán)過程之前發(fā)生,因?yàn)橄到y(tǒng)需要在分配其權(quán)限集之前標(biāo)識調(diào)用客戶端。
    • 但是某些身份驗(yàn)證技術(shù)(如持有者令牌)強(qiáng)制實(shí)施自包含的授權(quán)機(jī)制,從而允許服務(wù)器授權(quán)客戶端,而不必每次都對其進(jìn)行身份驗(yàn)證。
    • 持有者令牌的自包含授權(quán)方法在多功能性方面有幾個(gè)優(yōu)點(diǎn),但如果服務(wù)器或客戶端無法保護(hù)令牌免受第三方訪問,則可能會引發(fā)一些安全問題。
  • ASP.NET 核心標(biāo)識框架提供了一組豐富的 API 和高級抽象,可用于在任何 ASP.NET 核心應(yīng)用中管理和存儲用戶帳戶,這使其成為在任何 ASP.NET 核心應(yīng)用中實(shí)現(xiàn)身份驗(yàn)證和授權(quán)機(jī)制的絕佳選擇。
    • 此外,借助多個(gè)內(nèi)置類、幫助程序和擴(kuò)展方法,它可以輕松地與 EF Core 集成。
  • ASP.NET 核心標(biāo)識提供了多種執(zhí)行授權(quán)檢查的方法:
    • RBAC,它易于實(shí)現(xiàn),通常足以滿足大多數(shù)需求。
    • CBAC,實(shí)施起來稍微復(fù)雜一些,但用途更廣,因?yàn)樗梢杂脕頇z查任何索賠。
    • PBAC是RBAC和CBAC使用的基礎(chǔ)結(jié)構(gòu),可以直接訪問以設(shè)置更高級的授權(quán)要求。
  • 身份驗(yàn)證和授權(quán)是復(fù)雜的主題,尤其是從 IT 安全的角度來看,因?yàn)樗鼈兪呛诳凸?、DoS 攻擊和其他惡意活動的主要目標(biāo)。
    • 因此,應(yīng)非常謹(jǐn)慎地使用本章中描述的技術(shù),始終檢查更新,并與 ASP.NET 核心社區(qū)和IT安全標(biāo)準(zhǔn)提供的最佳實(shí)踐一起使用。

相關(guān)新聞

聯(lián)系我們
聯(lián)系我們
公眾號
公眾號
在線咨詢
分享本頁
返回頂部