一、前言
在涉及到後端專案的開發中,如何實現對於使用者許可權的管控是需要我們首先考慮的,在實際開發過程中,我們可能會運用一些已經成熟的解決方案幫助我們實現這一功能,而在 Grapefruit.VuCore 這個專案中,我將使用 Jwt 的方式實現對於使用者的許可權管控,在本章中,我將演示如何使用 Jwt 實現對於使用者的授權、鑒權。
系列目錄地址:ASP.NET Core 專案實戰
倉儲地址:https://github.com/Lanesra712/Grapefruit.VuCore
二、Step by Step
1、一些概念
Jwt(json web token),是一種基於 Json 的無狀態授權令牌,因為 Jwt 是一種標準的資料傳輸規範,並不是某家所獨有的技術規範,因此非常適用於構建單點登入服務,為 web、client、app 等等各種介面使用方提供授權服務。
在使用 Jwt 進行許可權控制的過程中,我們需要先請求授權伺服器獲取到 token 令牌,將令牌儲存到客戶端本地(在 web 專案中,我們可以將 token 儲存到 localstorage 或是 cookie 中),之後,對於服務端的每一次請求,都需要將獲取到的 token 資訊新增到 http 請求的 essay-header 中。
$.ajax({ url: url, method: "POST", data: JSON.stringify(data), beforeSend: function (xhr) { /* Authorization essay-header */ xhr.setRequestHeader("Authorization", "Bearer " + token); }, success: function (data) {} });
當使用者擁有令牌後是否就可以訪問系統的所有功能了呢?答案當然否定的。對於一個系統來說可能會有多種使用者角色,每一個使用者角色可以訪問的資源也是不同的,所以,當使用者已經擁有令牌後,我們還需要對使用者角色進行鑒定,從而做到對使用者進行進一步的許可權控制。
在 Grapefruit.VuCore 這個專案中,我採用的是基於策略的授權方式,透過定義一個授權策略來完善 Jwt 鑒權,之後將這個自定義策略註入到 IServiceCollection 容器中,對許可權控製做進一步的完善,從而達到對於使用者訪問許可權的管控。
基於策略的授權是微軟在 ASP.NET Core 中新增的一種新的授權方式,透過定義好策略(policy)的一個或多個要求(requirements),將這個自定義的授權策略在 Startup.ConfigureServices 方法中作為授權服務配置的一部分進行註冊之後即可按照我們的策略處理程式進行許可權的控制。
就像我在後面的程式碼中一樣,我定義了一個名叫 Permission 的授權策略,它包含了一個叫做 PolicyRequirement 的鑒權要求,在實現了授權策略後,將基於這個要求的鑒權方法 PolicyHandler 以單例(AddSingleton)的形式註入到服務集合中,此時,我們就可以在需要新增驗證的 controller 上新增 attribute 即可。
[Authorize(Policy = "Permission")] public class SecretController : ControllerBase {}
2、授權
在 Grapefruit.VuCore 這個專案中,涉及到授權相關的程式碼所在的位置我已在下圖中進行標示。在之前系列開篇文章(ASP.NET Core 實戰:使用 ASP.NET Core Web API 和 Vue.js,搭建前後端分離框架)進行介紹整個專案框架時曾說到, Grapefruit.Application 是專案的應用層,顧名思義,就是為了實現我們專案中的實際業務功能所劃分的類庫。因此,我們實現 Jwt 的相關業務程式碼應該位於此層中。同時,因為對於 Jwt 的令牌頒發與鑒權,採用的是微軟的 JwtBearer 元件,所以我們在使用前需要先透過 Nuget 將取用新增到 Grapefruit.Application 上。
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
Install-Package System.IdentityModel.Tokens.Jwt
在 Grapefruit.Application 這個類庫下我建立了一個 Authorization 解決方案檔案夾用來儲存授權相關的程式碼。在 Authorization 這個解決方案檔案夾中包含了兩個子檔案夾 Jwt 和 Secret。Jwt 檔案夾中主要包含我們對於 Jwt 的操作,而 Secret 檔案夾下則是對於使用者的相關操作。
每個子應用檔案夾(Jwt、Secret)都包含了相同的結構:Dto 資料傳輸物件、功能介面,以及功能介面的實現類,這裡介面的繼承採用單繼承的方式。
在 Jwt 檔案夾下建立一個 IJwtAppService 介面檔案,在這裡定義我們對於 Jwt 的相關操作。因為對於 Jwt 的授權、鑒權是採用微軟的 JwtBearer 元件,我們只需要進行配置即可,所以這裡只定義對於 token 的生成、掃清、停用,以及判斷這個 token 是否有效這幾個方法。同時,我們需要建立 JwtAppService 這個類檔案,去繼承 IJwtAppService 從而實現介面功能。
JwtAuthorizationDto 是一個 token 資訊的傳輸物件,包含我們建立好的 token 相關資訊,用來將 token 資訊傳回給前臺進行使用。而 UserDto 則是使用者登入獲取 token 時的資料傳輸物件,用來接收登入時的引數值。
在建立 token 或是驗證 token 時,像 token 的頒發者、接收者之類的資訊,因為會存在多個地方呼叫的可能性,這裡我將這些資訊存放在了配置檔案中,後面當我們需要使用的時候,只需要透過註入 IConfiguration 進行獲取即可。關於 Jwt 的配置檔案主要包含了四項:token 的頒發者,token 的接收者,加密 token 的 key 值,以及 token 的過期時間,你可以根據你自己的需求進行調整。
"Jwt": { "Issuer": "yuiter.com", "Audience": "yuiter.com", "SecurityKey": "a48fafeefd334237c2ca207e842afe0b", "ExpireMinutes": "20"}
在 token 的建立過程中可以簡單拆分為三個部分:根據配置資訊和使用者資訊建立一個 token,將加密後的使用者資訊寫入到 HttpContext 背景關係中,以及將建立好的 token 資訊新增到靜態的 HashSet 集合中。
在 token 建立、校驗的整個生命週期中,都涉及到了 Scheme、Claim、ClaimsIdentity、ClaimsPrincipal 這些概念,如果你之前有使用過微軟的 Identity 許可權驗證,對於這幾個名詞就會比較熟悉,可能某些小夥伴之前並沒有使用過 Identity,我來簡單介紹下這幾個名詞的含義。
Scheme 樣式,這個與其餘的名詞相對獨立,它主要是指明我們是以什麼授權方式進行授權的。例如,你是以 cookie 的方式授權或是以 OpenId 的方式授權,或是像這裡我們使用 Jwt Bearer 的方式進行授權。
Claim 宣告,以我們的現實生活為例,我們每個人都會有身份證,上面會包含我們的姓名、性別、民族、出生日期、家庭住址、身份證號,每一項資料的都可以看成是 type-value(資料型別-資料值),例如,姓名:張三。身份證上的每一項的資訊就是我們的 Claim 宣告,姓名:張三,是一個 Claim;性別:男,也是一個 Claim。而對於 ClaimsIdentity,就像這一項項的資訊最終組成了我們的身份證,這一項項的 Claim 最終組成了我們的 ClaimsIdentity。而 ClaimsPrincipal 則是 ClaimsIdentity 的持有者,就像我們擁有身份證一樣。
從上面的文字可以總結出,Claim(每一項的證件資訊)=》ClaimsIdentity(證件)=》ClaimsPrincipal(證件持有者)。其中,一個 ClaimsIdentity 可以包含多個的 Claim,而一個 ClaimsPrincipal 可以包含多個的 ClaimsIdentity。
如果想要深入瞭解 ASP.NET Core 的授權策略的可以看看園子裡這篇文章 =》ASP.NET Core 執行原理解剖[5]:Authentication,或是國外的這篇介紹 ASP.NET Core 授權的文章 =》Introduction to Authentication with ASP.NET Core。
實現 token 生成的最終程式碼實現如下所示,可以看到,在建立 ClaimsIdentity “證件”資訊時,我添加了使用者的角色資訊,並把加密後的使用者資訊寫入到 HttpContext 背景關係中,這樣,我們在後面驗證的時候就可以透過 HttpContext 獲取到使用者的角色資訊,從而判斷使用者是否可以訪問當前請求的地址。
當建立好 token 之後,客戶端就可以在 Http 請求的 essay-header 中新增 token 資訊,從而訪問受保護的資源。不過,在某些情況下,比如說,使用者修改了密碼之後,雖然當前的 token 資訊可能還未過期,但我們也不能允許使用者再使用當前的 token 資訊進行介面的訪問,這時,就涉及到了對於 token 資訊的停用以及掃清。
這裡我採用是當我們停用 token 資訊時,將停用的 token 資訊新增到 Redis 快取中,之後,在使用者請求時判斷這個 token 是不是存在於 Redis 中即可。
當然,你也可以在停用當前使用者的 token 資訊時,將 HashSet 中的這個 token 資訊進行刪除,之後,透過判斷訪問時的 token 資訊是否在 HashSet 集合中,判斷 token 是否有效。
方法很多,看你自己的需求了。
對於 Redis 的讀寫操作,我是使用微軟的 Redis 元件進行的,你可以按照你的喜好進行修改。如果你和我一樣,採用這個元件,你需要在 Grapefruit.Application 這個類庫中透過 Nuget 新增微軟的分散式快取抽象介面 dll 的取用,以及在 Grapefruit.WebApi 專案中新增微軟的 Redis 實現。
Install-Package Microsoft.Extensions.Caching.Abstractions ## 分散式快取抽象介面 Install-Package Microsoft.Extensions.Caching.Redis ## Redis 實現
當我們停用 token 時,透過 HttpContext 背景關係獲取到 HTTP Header 中的 token 資訊,將該 token 資訊儲存到 Redis 快取中,這樣,我們就完成了對於 token 的停用。
對於 token 的掃清,其實我們就可以看成重新生成了一個 token 資訊,只不過我們需要將之前的 token 資訊進行停用。
至此,我們對於 token 的建立、掃清、停用的程式碼就已經完成了,接下來,我們來實現對於 token 資訊的驗證。PS:下麵的程式碼如無特殊說明外,均位於 Startup 類中。
3、鑒權
在 ASP.NET Core 應用中,依賴註入隨處可見,而我們對於我們的功能方法的使用,也是採用依賴註入到容器,透過功能介面進行呼叫的方式。因此,我們需要將我們的介面與其實現類註入到 IServiceCollection 容器中。這裡,我們採用反射的方式,批次的將程式集內的介面與其實現類進行註入。
因為基礎的許可權驗證我們是採用的微軟的 JwtBearer 許可權驗證元件進行的授權和鑒權,因此對於 token 資訊的基礎鑒權操作,只需要我們在中介軟體中進行配置即可。同時,我們也在 IJwtAppService 介面中定義了對於 token 資訊的一些操作,而對於我們自定義的許可權驗證策略,則需要透過基於策略的授權方式進行實現。
首先,我們需要先定義一個繼承於 IAuthorizationRequirement 的自定義授權要求類 PolicyRequirement。在這個類中,你可以定義一些屬性,透過有參建構式的方式進行構造,這裡我不定義任何的屬性,僅是建立這個類。
public class PolicyRequirement : IAuthorizationRequirement { }
當我們建立好 PolicyRequirement 這個許可權要求類後,我們就可以透過繼承 AuthorizationHandler 來實現我們的授權邏輯。這裡實現許可權控制的程式碼邏輯,主要是透過重寫 HandleRequirementAsync 方法來實現的。
在判斷使用者是否可以訪問當前的請求地址時,首先需要獲取到使用者角色與其允許訪問的地址串列,這裡我使用的是模擬的資料。透過判斷當前登入使用者的角色是否包含請求的地址,當使用者的角色並不包含對於訪問地址的許可權時,傳回 403 Forbidden 狀態碼。
這裡需要註意,如果你準備採取 RESTful 風格的 API,因為請求的地址是相同的,你需要新增一個 HTTP 謂詞引數用來指明所請求的方法,從而達到訪問許可權管控的目的。。
包含 token 的基礎驗證的授權配置的程式碼如下所示。在中介軟體進行 Jwt 驗證的過程中,會驗證授權方式是不是 Bearer 以及透過 token 的屬性解密之後與生成時使用者資料進行比對,從而判斷這個 token 是否有效。
因為我們是使用 Swagger 進行的 API 檔案的視覺化,這裡,我們繼續配置 Swagger 從而使 Swagger 可以支援我們的許可權驗證方式。
在停用 token 的程式碼中,我們使用了 Redis 去儲存停用的 token 資訊,因此,我們需要配置我們的 Redis 連線。
現在,整個業務相關的程式碼已經完成了,我們可以建立前端訪問的介面了。這裡我是在 Controllers 下的 V1 檔案夾下建立了一個 SecretController 用來構建前端訪問的介面。控制器中主要有三個方法,分別為 CancelAccessToken(停用 token)、Login(獲取 token)以及 RefreshAccessTokenAsync(掃清 token)。
現在,讓我們測試一下,從下圖中可以看到,當我們未獲取 token 時,訪問介面提示我們 401 Unauthorized,當我們模擬登入獲取到 token 資訊後,再次訪問受保護的資源時,已經可以獲取到響應的資料。之後,當我們掃清 token,此時再用原來的 token 資訊訪問時,已經無法訪問,提示 403 Forbidden,同時,可以看到我們的 Redis 中已經存在了停用的 token 資訊,此時,使用新的 token 資訊又可以訪問了。
至此,整個的 Jwt 授權鑒權相關的程式碼就已經完成了,因為篇幅原因,完整的程式碼請到 Github 上進行檢視(電梯直達)。PS:因為部落格園允許上傳的圖片限制最大尺寸為 10M,所以這裡上傳的 gif 是壓縮後的,見諒見諒,如果有需要檢視清晰的圖片,歡迎到我的個人部落格上檢視(電梯直達)。
、總結
本章,主要是使用 Jwt 完成對於使用者的授權與鑒權,實現了對於使用者 token 令牌的建立、掃清、停用以及校驗。在實際的開發中,採用成熟的輪子可能是更好的方案,如果你有針對 Jwt 進行使用者授權、鑒權更好的解決方案的話,歡迎你在評論區留言指出。拖了很久,應該是年前的最後一篇了,提前祝大家新年快樂哈~~~