操屁眼的视频在线免费看,日本在线综合一区二区,久久在线观看免费视频,欧美日韩精品久久综

新聞資訊

    章涵蓋

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

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

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

    9.1 基本概念

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

    9.1.1 身份驗證

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

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

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

    問 責(zé)

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

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

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

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

    提示有關(guān) ASP.NET Core 應(yīng)用中身份驗證的更多詳細信息,請查看 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)限)分配給單個計算機,軟件或用戶(或組)的過程。這些任務(wù)通常通過實現(xiàn)訪問策略、聲明或權(quán)限組來處理,這些策略、聲明或權(quán)限組允許或禁止一組給定邏輯空間(文件系統(tǒng)文件夾、驅(qū)動器網(wǎng)絡(luò)、數(shù)據(jù)庫、網(wǎng)站部分、Web API 終結(jié)點等)中的每個相關(guān)操作或活動(讀取、寫入、刪除等)。實際上,通常通過定義一系列訪問控制列表 (ACL) 來提供或拒絕授權(quán),這些列表指定

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

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

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

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


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

    在圖中,身份驗證過程應(yīng)在授權(quán)之前進行,因為后者需要前者來執(zhí)行其工作。但這種情況不一定是真的。如果匿名用戶嘗試訪問受限資源,授權(quán)系統(tǒng)將在身份驗證之前啟動,拒絕對未經(jīng)身份驗證的用戶的訪問,并可能驅(qū)動 Web 應(yīng)用程序?qū)⒂脩糁囟ㄏ虻降卿涰撁妗T谀承┻吘壡闆r下,甚至可能存在只能由匿名用戶(未經(jīng)身份驗證的用戶)訪問的資源。一個典型的示例是登錄頁面,因為在注銷之前,絕不應(yīng)允許經(jīng)過身份驗證的用戶執(zhí)行其他登錄嘗試。

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

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

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

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

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

    實現(xiàn)方法

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

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

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

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

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

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

    不記名令牌

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

    基于令牌的身份驗證仍要求用戶使用用戶名和密碼對自己進行身份驗證(執(zhí)行登錄)。但是,身份驗證過程成功后,服務(wù)器不會創(chuàng)建持久會話,而是生成一個加密的授權(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é)了這一過程。


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

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

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

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

    9.2 ASP.NET 核心身份

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

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

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

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

    9.2.1 安裝 NuGet 包

    若要將 ASP.NET 核心標(biāo)識功能添加到項目中,我們需要以下 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)航到項目的根文件夾,然后鍵入以下命令:

    > 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)建用戶實體

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

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

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

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

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

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

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

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

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

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

    ? 必需的命名空間

    ? 新的 IdentityDbContext<TUser> 基類

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

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

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

    > dotnet ef migrations add Identity

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

    > dotnet ef database update Identity

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


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

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

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

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

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

    如果我們想更改表名,我們可以通過重寫 ApplicationDbContext 的 OnModelCreate 方法中的默認值來實現(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í)行此操作;我們將保留默認前綴。

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

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

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

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

    添加身份服務(wù)

    以下是我們需要做的:

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

    打開 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ù)

    ? 配置密碼強度要求

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

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

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

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

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

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

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

    清單 9.2 程序.cs文件:認證服務(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"])      )
        };
    });

    ? 必需的命名空間

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

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

    ? 添加 JWT 持有者身份驗證方案

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

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

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

    更新 appsettings.json 文件

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

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

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

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

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

    添加身份驗證中間件

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

    app.UseAuthorization();

    并在其前面添加 ASP.NET Core 身份驗證中間件:

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

    ? 新的身份驗證中間件

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

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

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

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

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

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

    讓我們從控制器本身中的空樣板開始。在項目的 /Controllers/ 文件夾中創(chuàng)建一個新的 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ī)則(“[控制器]/[操作]”)定義了一個 [Route] 屬性,因為我們必須處理需要區(qū)分的兩個 HTTP POST 方法。由于該規(guī)則,我們的方法將具有以下端點:

    /Account/Register
    /Account/Login

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

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

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


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

    此表用于存儲我們之前創(chuàng)建的 ApiUser 實體的記錄,該實體是 IdentityUser 默認類的擴展。如果我們檢查該實體,我們會看到它對每個表列都有一個公共屬性,這并不奇怪,因為我們首先使用了 EF Core 代碼優(yōu)先方法來創(chuàng)建表。

    現(xiàn)在我們知道了我們需要從想要創(chuàng)建新帳戶的用戶那里獲取的數(shù)據(jù),我們可以實現(xiàn) DTO 對象來“傳輸”他們,從而將第 6 章中的課程付諸實踐。在項目的 /DTO/ 文件夾中創(chuàng)建一個新的 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,我們可以使用它來實現(xiàn)我們的 帳戶控制器 。注冊方法,預(yù)期處理以下任務(wù):

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

    下面的清單顯示了我們?nèi)绾螌崿F(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)措施

    ? 捕獲任何異常并返回錯誤

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

    創(chuàng)建測試用戶

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


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

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

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

    請求應(yīng)返回 HTTP 狀態(tài)代碼 400,并帶有解釋錯誤原因(電子郵件格式無效)的響應(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)驗證工作正常。目前為止,一切都好。現(xiàn)在,讓我們修復(fù)電子郵件字段并使用以下值執(zhí)行新測試:

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

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

    {
      "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')."
    }

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

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

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

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

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

    實現(xiàn)登錄方法

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

    清單 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)在我們可以實現(xiàn) AccountController.Login 方法,該方法需要處理以下任務(wù):

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

    下面的清單包含這些任務(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è)置用戶聲明

    ? 實例化 JWT 對象實例

    ? 生成 JWT 加密字符串

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

    ? 捕獲任何異常并返回錯誤

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

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

    注意我們正在使用其他幾個UserManager方法:FindByNameAsync和CheckPasswordAsync。因為他們的名字是不言自明的,所以理解他們做什么應(yīng)該不難。

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

    對測試用戶進行身份驗證

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


    圖 9.6 SwaggerUI 中的 /帳戶/登錄端點

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

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

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


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

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

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

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

    • 客戶端 - 添加包含 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)頭

    我們將要實現(xiàn)的技術(shù)旨在模擬實際的 REST 客戶端在執(zhí)行請求時會執(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 的保護類型,以及全局強制實施的新安全要求。以下清單顯示了如何操作。

    示例 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)一個彈出窗口,讓我們有機會插入要在授權(quán) HTTP 標(biāo)頭中使用的持有者令牌。


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

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

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

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

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

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

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

    由于我們尚未為用戶定義任何策略或角色,因此可以使用屬性的無參數(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)過身份驗證的用戶才能訪問。

    選擇默認訪問行為

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

    這兩種行為都是可行的,具體取決于特定的用例。一般而言,限制性更強的方法(默認阻止,選擇允許)被認為更安全,不易發(fā)生人為(開發(fā)人員)錯誤。通常,忘記 [Authorize] 屬性比忘記 [AllowAnonymous] 屬性更糟糕,因為它很容易導(dǎo)致數(shù)據(jù)泄露。

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

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

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

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

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

    示例 9.9 程序.cs文件:/auth/test/1 最小 API 端點

    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)試模式下啟動項目,然后訪問 SwaggerUI 儀表板。與往常一樣,此客戶端是我們將用來執(zhí)行測試的客戶端。

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


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

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

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

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


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

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

    9.4 基于角色的訪問控制

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

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

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

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

    實施 RBAC 策略是處理任務(wù)的好方法,因為它允許我們對用戶進行分類。簡而言之,這是我們需要做的:

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

    9.4.1 注冊新用戶

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

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

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

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

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

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

    9.4.2 創(chuàng)建新角色

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

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

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

    添加角色名稱常量

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

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

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

    這些常量將允許我們每次都使用強類型方法而不是文字字符串來引用我們的角色,從而防止人為錯誤。現(xiàn)在,我們可以編寫代碼來創(chuàng)建這些角色。因為我們談?wù)摰氖且粋€可能只執(zhí)行一次的數(shù)據(jù)庫種子任務(wù),所以最好的地方是放在我們的 SeedController 中。但是使用現(xiàn)有的 Put 方法(我們在第 5 章中實現(xiàn)了該方法,將棋盤游戲數(shù)據(jù)插入數(shù)據(jù)庫)將是一種不好的做法,因為它會破壞單一責(zé)任原則。相反,我們應(yīng)該重構(gòu) SeedController 為我們希望它處理的兩個任務(wù)創(chuàng)建不同的端點(和操作方法)。我們將重命名現(xiàn)有終結(jié)點 /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 code
    namespace 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ù)

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

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

    請注意,我們沒有實現(xiàn)新方法,而是專注于 SeedController 的重構(gòu)部分。現(xiàn)在我們可以繼續(xù)實現(xiàn) AuthData 操作方法,將“未實現(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)角色尚不存在時,才會創(chuàng)建角色。
    • 僅當(dāng)用戶存在且尚未加入時,才會將用戶添加到角色中。

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

    提示Test管理員用戶已添加到多個角色:審閱者和管理員。這對于我們的任務(wù)來說完全沒問題,因為我們希望管理員擁有與版主相同的權(quán)限。

    9.4.3 為用戶分配角色

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

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

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

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

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

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

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

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

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

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

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

    [Authorize(Roles=RoleNames.Moderator)]

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

    using MyBGList.Constants;

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

    [Authorize(Roles=RoleNames.Administrator)]

    然后打開 SeedController,并使用前面的屬性更新其 [Authorize] 屬性(我們將其應(yīng)用于控制器本身),因為將該控制器限制為管理員是我們分配的一部分。

    9.4.6 測試 RBAC 流

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

    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)榈谝粋€授權(quán)流設(shè)計的測試周期非常相似。在 SwaggerUI 主儀表板中,執(zhí)行以下步驟:

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

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

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

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

    基于聲明的訪問控制

    此方法稱為基于聲明的訪問控制CBAC),包括 RBAC 提供的相同功能以及更多功能,因為它可用于同時檢查任何聲明(或聲明集)。

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

    與 RBAC 不同,RBAC 由于 [Authorize] 屬性的 Role 屬性而可以輕松快速地實現(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文件中,以配置身份驗證服務(wù)。然后我們可以通過以下方式將策略設(shè)置為 [Authorize] 屬性的參數(shù):

    [Authorize(Policy="ModeratorWithMobilePhone")]

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

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

    我不打算在本書中使用這種方法。我簡要展示它只是因為它對于實現(xiàn)某些特定的授權(quán)要求很有用。

    基于策略的訪問控制

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

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

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

    我不打算在本書中使用 PBAC,因為它需要實現(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)有的代碼進行大量重構(gòu),因為我們目前不詢問(并收集)注冊用戶的出生日期。出于這個原因,我將在這里停止,將前面的代碼僅供參考。

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

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

    9.5 習(xí)題

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

    9.5.1 添加新角色

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

    9.5.2 創(chuàng)建新用戶

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

    9.5.3 為用戶分配角色

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

    9.5.4 實現(xiàn)測試端點

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

    9.5.5 測試 RBAC 流

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

    總結(jié)

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

    可能是將一個html 表格變成 Microsoft Excel 格式的最快方法。ContentType 屬性通知
    瀏覽器數(shù)據(jù)要被格式化為何種格式,在這里我們要的格式是Microsoft Excel。當(dāng)瀏覽器看到這個屬性的值是Excel時,它就提示用戶保存或打開這個文件。如果用戶選擇打開文件,
    就啟動了Excel并在其中觀看數(shù)據(jù)。為使其工作正確,必須在向Response對象寫入任何內(nèi)容之前設(shè)置ContentType 。此語法的例子如下:
       Line 1: 〈 %@ LANGUAGE="VBSCRIPT" % 〉
       Line 2: Response.ContentType="application/msexcel"
       Line 3: % 〉
       點擊這里可以得到有關(guān)ASP的Response 對象的ContentType屬性的更多信息。
       當(dāng)我試圖用Internet Explorer 4.x.測試時發(fā)現(xiàn)了一個問題,在Microsoft文章 Q185978曾經(jīng)提到過。 這篇知識庫文章的內(nèi)容可以概括如下:
       “如果Internet Explorer 與一個動態(tài)生成Word、 Excel或其它活動文檔的Web服務(wù)器
    資源相連接時,Internet Explorer會為此資源發(fā)出兩個GET 請求。第二個GET 通常沒有ses
    sion 狀態(tài)信息、臨時 cookies或者已經(jīng)為客戶指定的證明信息。這個錯誤可能影響到任何
    寄宿在Internet Explorer 結(jié)構(gòu)窗口內(nèi)的本地服務(wù)器(EXE) 的活動文檔應(yīng)用程序。它在ISAP
    I、 ASP或 CGI 應(yīng)用程序中發(fā)生最為頻繁,它們校驗HTTP "Content Type" 頭文件以識別所
    安裝的應(yīng)用程序”。
       因此如果你嘗試使用session 變量或 cookies, 并使用IE4,就有可能遭遇到這個錯
    誤。經(jīng)證實,在IE5中這個問題已經(jīng)得到解決。
       一個用逗號分隔開的值文件是將web頁面輸出到Excel可讀格式的第二種選擇。這種格
    式比ContentType 屬性有更大的靈活性。相對于其它方法,CSV還有兩個優(yōu)勢:首先,不需
    要任何客戶機或服務(wù)器上的 軟件去創(chuàng)建它,第二,文件通常要比一個Excel 文件小。
       CSV格式的定義如下:逗號分隔列,回車分隔行。逗號作為分隔符, 也會與包含逗號
    的 域(如,234)引發(fā)一個問題; 這會在將要創(chuàng)建的行中導(dǎo)致一個額外的列。這個問題也
    很容易矯正, 方法是在每個結(jié)尾處用逗號將域封閉起來。
       在提供的例子中把 CreateCSVFile()函數(shù)過一遍,就能了解CSV文件是如何創(chuàng)建的。
       Line 1: strFile=GenFileName()
       Line 2: Set fs=CreateObject("Scripting.FileSystemObject")
       Line 3: Set a=fs.CreateTextFile(server.MapPath(".") & "" & strFile &
    Line 4: ".csv",True)
       Line 5: If Not oRS.EOF Then
       Line 6: strtext=chr(34) & "Year" & chr(34) & ","
       Line 7: strtext=strtext & chr(34) & "Region" & chr(34) & ","
       Line 8: strtext=strtext & chr(34) & "Sales" & chr(34) & ","
       Line 9: a.WriteLine(strtext)
       Line 10: Do Until oRS.EOF
       Line 11: For i=0 To oRS.fields.Count-1
       Line 12: strtext=chr(34) & oRS.fields(i) & chr(34) Line 13: & ","
       Line 14: a.Write(strtext)
       Line 15: Next
       Line 16: a.Writeline()
       Line 17: oRS.MoveNext
       Line 18: Loop
       Line 19: End If
       Line 20: a.Close
       Line 21: Set fs=Nothing
      Line 22:Response.Write("Click〈 A HREF=" & strFile & ".csv 〉Here〈 /A 〉
       Line 23: to get CSV file")
       第一行調(diào)用GenFileName() 函數(shù)創(chuàng)建一個唯一的文件名,有關(guān)GenFileName() 函數(shù)將
    在稍后討論。
       第2行到第4行,用FileSystemObject 對象和CreateTextFile函數(shù)將要寫入的文本文
    件。在這個例子中, 所寫入的文件與源文件在同一個路徑下,在實際工作中,你也許想要
    創(chuàng)建一個單獨的路徑存儲這些文件。
       第5-9行產(chǎn)生第一行的標(biāo)題。因為報告通常都是相同的,我就把列名的代碼固定下來,
    雖然也有可能讀 數(shù)據(jù)庫的列名并使用它們。注意我在各個域中是如何包含逗號的。使用Wri
    teLine 函數(shù)將它們與一個回 車一起發(fā)送到文件中。
       第10行到18行在記錄集中循環(huán),用引號給每個域做出標(biāo)志,后面跟著一個逗號。然后W
    rite函數(shù)將每個 域發(fā)送到文件。WriteLine 用回車結(jié)束每一行。
       最后幾行關(guān)閉文件、釋放對象、在頁面上放置一個鏈接以便能夠找回它。
       當(dāng)你點擊生成的鏈接時,就會被提示保存或打開。如果選擇打開,文件就在Excel 中
    打開(假設(shè)計算機 上已經(jīng)安裝)。如果選擇了保存,就將這個文本文件保存到存儲設(shè)備上
    并將它輸入各個應(yīng)用程序中。
       我所討論的最后一種方法是用Microsoft Excel 對象創(chuàng)建一個實際的Excel(.xls ) 文
    件。要使用這些對象要求在Web 服務(wù)器上安裝Excel。使用這些控制可以對格式化有更多的
    控制(如字體、顏色等), 并允許你進行一切在真正的Excel 應(yīng)用程序中可以進行的操
    作。一定要監(jiān)視你的服務(wù)器的性能,因為 Excel 可能成為一個相當(dāng)大的對象并對性能造成
    沖擊,這取決于你如何使用它以及服務(wù)器有多忙。
       我發(fā)現(xiàn)在Excel 對象上得到更多信息的最快最簡便的方法是使用Visual Basic對象瀏
    覽器,觀看對象 并使用上下文敏感幫助來得到更多細節(jié)。使用這個對象瀏覽器時:啟動Vis
    ual Basic, 創(chuàng)建一個工程文件, 增加一個對Microsoft Excel對象庫的引用。在 View 菜
    單下,可以選擇一個對象瀏覽器然后指定Excel 庫,看到所有可用的對象。按 F1可得到當(dāng)
    前標(biāo)題的上下文敏感幫助。
       在所提供的樣本中把CreateXlsFile() 函數(shù)過一遍,就可以看到如何創(chuàng)建一個Excel文
    件。 基本步驟與 創(chuàng)建 CSV文件的基本相同,只是所創(chuàng)建的是一個Excel工作表。
    Line 1: Dim xlWorkSheet
    Line 2: Dim xlApplication
    Line 3: Set xlApplication=Server.CreateObject("Excel.Application")
    Line 4: xApplication.Visible=False
    Line 5: xlApplication.Workbooks.Add
    Line 6: Set xlWorksheet=xlApplication.Worksheets(1)
    Line 7: xlWorksheet.Cells(1,1).Value="Year"
    Line 8: xlWorksheet.Cells(1,1).Interior.ColorIndex=5
    Line 9: xlWorksheet.Cells(1,2).Value="Region"
    Line 10: xlWorksheet.Cells(1,2).Interior.ColorIndex=5
    Line 11: xlWorksheet.Cells(1,3).Value="Sales"
    Line 12: xlWorksheet.Cells(1,3).Interior.ColorIndex=5
    Line 13: iRow=2
    Line 14: If Not oRS.EOF Then
    Line 15: Do Until oRS.EOF
    Line 16: For i=0 To oRS.fields.Count-1
    Line 17: xlWorksheet.Cells(iRow,i + 1).Value=oRS.fields(i)
    Line 18: xlWorkSheet.Cells(iRow,i + 1).Interior.ColorIndex=4
    Line 19: Next
    Line 20: iRow=iRow + 1
    Line 21: oRS.MoveNext
    Line 22: Loop
    Line 23: End If
    Line 24: strFile=GenFileName()
    Line 25: xlWorksheet.SaveAs Server.MapPath(".") & "" & strFile & ".xls"
    Line 26: xlApplication.Quit ' Close the Workbook
    Line 27: Set xlWorksheet=Nothing
    Line 28: Set xlApplication=Nothing
    Line 29: Response.Write("Click 〈 A HRef=" & strFile & ".xls 〉Here〈 /A 〉
    to Line 30: get XLS file")
       第1行和第2行,確定所使用的 Excel對象的維數(shù)。
       第3行,創(chuàng)建Excel對象。同樣,為了工作正確,web服務(wù)器上也必須有Excel。
       第4行,將Excel的可見性設(shè)置為false,這樣它就沒有界面了。
       第5行和第6行,增加一個容納工作表的工作簿,然后將當(dāng)前工作表設(shè)置成第一個工作
    表(這是Excel 在默認狀態(tài)下創(chuàng)建的)。 還可以用 Worksheet對象的Add 函數(shù)增加一個新的
    工作表, 這就允許你的 Excel 文件中有多個工作表。
       第8-12行,創(chuàng)建工作表的標(biāo)題。在這個例子中,我們把每個單元的值都設(shè)置成適當(dāng)?shù)?br>標(biāo)題,而且把內(nèi)部 顏色設(shè)置成蘭色。你還可以用Range對象同時修改多個單元。
       第13-23行,提供從記錄集裝載所有數(shù)據(jù)的循環(huán)。因為第一行中包含標(biāo)題,我就在電子
    數(shù)據(jù)表的 第二行開始數(shù)據(jù)。里面的 For循環(huán)把每一列裝載到行中,并把內(nèi)部顏色設(shè)置為綠
    色。外部循環(huán)則為 每一行在記錄集中進行循環(huán)。
       第24行, 通過調(diào)用GenFileName()函數(shù),與CSV用同樣的函數(shù)來創(chuàng)建唯一的文件名。
       第25行,進行電子表格的實際保存。可以將表格存儲為 Excel中指定的多種格式。
       下面的3行進行對象的整理。作為一個好的ASP程序員,就一定要整理所有的對象。
       最后,我把到 Excel文件的鏈接放在頁面上以便下載。
       我創(chuàng)建了一個樣本,對以上討論過的每個技巧進行示范。要安裝樣本,只需要把所有
    的文件復(fù)制到服務(wù)器上,用 main.html 啟動應(yīng)用程序。在服務(wù)器上需要有 Excel以使用"Na
    tive Excel" 選項。 樣本使用一個Access數(shù)據(jù)庫 (無DSN鏈接)來存儲銷售數(shù)據(jù)。
       你可以選擇一年或一個地區(qū)進行銷售報告。最后的選項是你希望如何返回數(shù)據(jù)。可以
    看到以下的屏幕映象:
    <p align="center">
       下面的表格中是對樣本中提供的所有文件的描述。
    文件名 描述
    DSN-SQL.asp 包含無DSN鏈接字符串
    adovbs.inc 包含ADO常量
    TestDB.mdb 包含銷售數(shù)據(jù)的一個Access97數(shù)據(jù)庫。包含的銷售表格有3個域:year--Tex
    t, region--Text,sales-amt--numeric
    main.html 本文件創(chuàng)建畫面的框架并裝載初始頁
    welcome.html 本文件只在第一次創(chuàng)建結(jié)果通常所在的畫面框架時使用
    request.html 包含一個表單,用來收集用戶的選擇來建立報告
    runquery.asp 應(yīng)用程序的內(nèi)臟。本文件建立SQL聲明、確定客戶機如何請求將被返回的數(shù)
    據(jù)、執(zhí)行SQL并按照請求返回數(shù)據(jù)
       大部分代碼都相當(dāng)容易理解。但是我還是要討論runquery.asp 文件中的一些函數(shù)。我
    已經(jīng)演示過如何創(chuàng)建CSV和Excel 文件。
       GenHTML()函數(shù)建立一個被請求數(shù)據(jù)的HTML表格。這個函數(shù)既用來作為HTML返回也用于
    ContentType請求。為了 ContentType請求工作,你要注意 Response.ContentType="appl
    ication/msexcel" 是將要執(zhí)行的最初幾行之一。
       GenFileName()函數(shù)使用系統(tǒng)日期建立文件名的第一部分。這個文件名將是唯一的,這
    樣當(dāng)你試圖存儲 文件時就可以避免許多麻煩。擴展名( CSV或XLS )在存儲文件時應(yīng)用,這
    樣就允許同一個函數(shù)產(chǎn)生兩種類型的文件。
       BuildSQL()函數(shù)使用表單變量來建立一個SQL聲明,與用戶的請求相匹配,并將其返回
    調(diào)用者。
       Recordset在腳本的最頂部被打開,因為它對于所選擇的顯示類型是獨立的。recordse
    t處理從BuildSQL()函數(shù)調(diào)用生成的SQL聲明,使用一個到Access 97的無DSN鏈接。
       〈 BODY 〉標(biāo)記中包含的代碼僅僅是兩個 "if….then" 聲明,確定用戶所要求的顯示
    方法。 if聲明分流到生成正確返回類型的函數(shù),該返回類型是基于用戶的"ReturnAS" 選
    擇。接著清除鏈接和記錄集對象。
       注意: 這個樣本沒有涉及到用戶下載web服務(wù)器上創(chuàng)建的文件之后,對這些文件的維護
    問題。我建議 這種維護要基于一段時間,時間到期后就刪除這些文件。 我不主張把移走這
    些文件的負擔(dān)轉(zhuǎn)嫁到客戶身上(通過頁面上的鏈接),因為他們很容易忘記這些事情。


       結(jié)論
       本文演示了將數(shù)據(jù)輸出到一個Excel可讀格式的三種方法。 我相信根據(jù)用戶的不同需
    要,這三種方法都有其可用之地。如果你愿意快速但不漂亮地輸出到Excel,就用ContentTy
    pe好了。如果你想要一個格式有限但能夠裝載到許多不同應(yīng)用程序中的文件,那么CSV格式
    適合你。如果你更喜歡包含完整格式、圖標(biāo)或特殊Excel功能,那么創(chuàng)建一個完全的Excel電
    子表格是適合的途徑。 但愿這些方法能幫助其他程序員滿足客戶的要求或者至少幫助你選
    擇正確的途徑。

網(wǎng)站首頁   |    關(guān)于我們   |    公司新聞   |    產(chǎn)品方案   |    用戶案例   |    售后服務(wù)   |    合作伙伴   |    人才招聘   |   

友情鏈接: 餐飲加盟

地址:北京市海淀區(qū)    電話:010-     郵箱:@126.com

備案號:冀ICP備2024067069號-3 北京科技有限公司版權(quán)所有