前言 預計是通過三篇來將清楚asp.net core 3.x中的授權:1、基本概念介紹;2、asp.net core 3.x中授權的預設流程;3、擴展。 在完全沒有概念的情況下無論是看官方文檔還是源碼都暈乎乎的,希望本文能幫到你。不過我也是看源碼結合官方文檔看的,可能有些地方理解不對,所以只作為參考 ...
前言
預計是通過三篇來將清楚asp.net core 3.x中的授權:1、基本概念介紹;2、asp.net core 3.x中授權的預設流程;3、擴展。
在完全沒有概念的情況下無論是看官方文檔還是源碼都暈乎乎的,希望本文能幫到你。不過我也是看源碼結合官方文檔看的,可能有些地方理解不對,所以只作為參考。
要求對asp.net core的基礎有所有瞭解:Host、依賴註入、日誌、配置、選項模式、終結點路由、身份驗證等。還是推薦A大博客
概述
歸納來說授權就是:某人 針對某個資源 可以做什麼操作,如:張三 針對銷售訂單 可以查看、審核、取消等操作
- 某人:這個好理解,只要登錄系統的用戶我們就曉得他是誰;額外的他屬於某個角色、屬於某個部門、甚至我們可以規定年齡段在18-30歲的能幹什麼,30-50歲的能幹啥,這些都屬於所屬角色、所屬部門、所屬年齡段都是用戶的一個屬性,會作為許可權判斷的一個依據
- 資源:可以任何形式的資源,比如銷售訂單、商品、等等;也可以有更複雜的規則,比如金額大於10000以上的,必須經過老總審核這種要求;另外比如一個頁面也可以看做是資源,比如是否允許誰可以訪問某個頁面。對資源的限定也將作為許可權判斷的一部分
- 操作:比如上面說的查看、審核、新增、修改..巴拉巴拉...當然操作也作為許可權判斷的一部分。
除了上面這3個概念外加一個許可權判斷邏輯,就組成了授權系統。下麵逐一介紹asp.net core 3.x中授權系統涉及到的相關概念
註:
功能許可權:這是我們通常說的某人(包含所屬角色,所屬部門等)可以訪問某個菜單下的某個按鈕
數據許可權:上面說的訂單金額大於10000的必須要經理角色才可以審核
這個說來也沒啥區別,一個按鈕通常是對應到mvc中的某個action,所以還是可以看成是操作;金額大於10000,這個也只是對資源的一種選定
還有一種情況,說一個按鈕的點擊對應到一個action,那麼這個按鈕到底是看做“操作”呢,還是把這個Action看成是一個頁面地址,作為資源呢?這個就看怎麼設計了,mvc預設是當做資源。
用戶標識ClaimsPrincipal
現實生活中,一個人有多種證件,比如身份證、登機牌、就診卡等,你去到不同的機構需要出示不同的證件,而每張證件上又有不同的信息,比如身份驗證上有身份證號、姓名、性別、出示日期等等... 登機牌上有 航班號、座位號之類的。
在asp.net core中,ClaimsPrincipal就代表上面說的這個人,它可能存在多張證件,證件用ClaimsIdentity表示,當然得有一張證件作為主要證件(如身份證);一張證件又包含多條信息,可以用類似字典的形式IDictionary<string,string>來存儲證件的信息,但是字典不夠面向對象,所以單獨為證件上的一條信息定義了一個類Claim,拿身份證上的出生日期來說,ClaimType="出生日期",Value=“1995-2-4”
上面我們一直拿一個人擁有多張證件來舉例,其實並不准確,因為對系統來說並不關心是誰登錄,可能是一個用戶、也可能是一個第三方應用。所以將ClaimsPrincipal理解為一個登錄到系統的主體更合理。
在一個系統中可能同時存在多種身份驗證方案,比如我們系統本身做了用戶管理功能,使用最簡單的cookie身份驗證方案,或者使用第三方登錄,微信、QQ、支付寶賬號登錄,通常一個身份驗證方案可以產生一張證件(ClaimsIdentity),當然某個身份驗證方案也可以將獲得的Claim添加到一張現有的證件中,這個是靈活的。預設情況下,用戶登錄時asp.net core會選擇設置好的預設身份驗證方案做身份驗證,本質是創建一個ClaimsPrincipal,並根據當前請求創建一個證件(ClaimsIdentity),然後將此ClaimsIdentity加入到ClaimsPrincipal,最後將這個ClaimsPrincipal設置到HttpContext.User屬性上。身份驗證不是本篇重點,詳細描述參考:《asp.net core 3.x 身份驗證-1涉及到的概念》。我們目前只要記住一個字元串代表一個身份驗證方案,它可以從當前請求或第三方去獲得一張證件(ClaimsIdentity)
當用戶登錄後,我們已經可以從HttpContext.User拿到當前用戶,裡面就包含一張或多張證件,後續的許可權判斷通常就依賴裡面的信息,比如所屬角色、所屬部門,除了證件的信息我們也可以通過用戶id去資料庫中查詢得到更多用戶信息作為許可權判斷的依據。
資源
資源的概念很寬泛,上面說的銷售訂單、客戶檔案、屬於資源,我們可以控制某個用戶是否能查看、新增、審核訂單。或者說一個頁面也是一種資源,我們希望控制某用戶是否能訪問某個頁面。在asp.net core中直接以object類型來表示資源,因為asp.net core作為一個框架,它不知道將來使用此框架的開發者到底是對什麼類型的資源做許可權限制。
在我們日常開發中經常在Action上應用AuthorizeAttribute標簽來進行授權控制,其實這裡就是將這個Action當做資源。由於目前asp.net core 3.x中預設使用終結點路由,所以現在在asp.net core 3.x中的預設授權流程中當前Endpoint就是資源
記住許可權判斷中不一定需要資源的參與,比如只要用戶登錄,就允許使用系統中所有功能。此時整個系統就是資源,允許所有操作。
核心概念圖
授權依據IAuthorizationRequirement
試想這樣一種許可權需求:要求屬於角色"經理"或"系統管理員"的用戶可以訪問系統任何功能。當我們做許可權判斷時我們可以從HttpContext.User得到當前用戶,從其證件列表中總能找到當前用戶的所屬角色,那麼這裡需要進行比較的兩個角色"經理"、"系統管理員"從哪裡獲得呢?
再比如:要求只要當前用戶的證件中包含一個"特別通行證"的Calim,就允許他訪問系統的任何功能。同上面的情況一樣,在判斷許可權時我們可以知道當前登錄用戶的Calim列表,那需要進行比對的"特別通行證"這個字元串從哪來呢?
asp.net core將這種許可權判斷時需要用來比對的數據定義為IAuthorizationRequirement,我這裡叫做"授權依據",在一次許可權判斷中可能會存在多個判斷,所以可能需要多個授權依據,文件後面會講如何定製授權依據
其實某種意義上說“當前用戶(及其包含的Calim列表)”也可以看做是一種依據,因為它也是在授權判斷過程中需要訪問的數據,但是這個我們是直接通過HttpContext.User來獲取的,不需要我們來定義。
當我們針對某個頁面或Action進行授權時可以直接從當前路由數據中獲取Action名,在asp.net core 3.x中甚至更方便,可以在請求管道的早期就能獲得當前請求的終結點。所以針對Action的訪問也不需要定義成授權依據中
所以授權依據算是一種靜態數據,為了更好的理解,下麵列出asp.net core中已提供的幾種授權依據
- ClaimsAuthorizationRequirement
public string ClaimType { get; } public IEnumerable<string> AllowedValues { get; }
將來在許可權判斷是會判斷當前用戶的Claim列表中是否包含一個類型為ClaimType的Claim,若AllowedValues有數據,則進一步判斷是否完整包含AllowedValues中定義的值
- DenyAnonymousAuthorizationRequirement:許可權判斷發現存在這個依據,則直接拒絕匿名用戶訪問
- RolesAuthorizationRequirement:這就是最常見的基於角色的授權時會使用的,它定義了 public IEnumerable<string> AllowedRoles { get; } ,將來做許可權判斷時會看當前用戶是否屬於這裡允許的角色中的一種
- OperationAuthorizationRequirement:這個也比較常用,在做功能授權時比較常用。它定義了 public string Name { get; set; } ,Name代表當前操作名,比如“Order.Add”就是新增訂單,將來許可權判斷是可以根據當前用戶Id、所屬角色和"Order.Add"到資料庫去做對比
- AssertionRequirement:這個就更強大了,它定義了 public Func<AuthorizationHandlerContext, Task<bool>> Handler { get; } ,將來許可權判斷時發現是這個類型,直接調用這個委托來進行許可權判斷,所以靈活性非常大
授權策略AuthorizationPolicy
策略同時作為身份驗證方案和授權依據的容器,它包含本次授權需要的數據。
請求抵達時asp.net core會找到預設身份驗證方案進行身份驗證(根據請求獲取用戶ClaimsPrincipal),但有時候我們希望由自己來指定本次授權使用哪些身份驗證驗證方案,而不是使用預設的,這樣將來身份驗證過程中會調用設置的這幾個身份驗證方案去獲得多張證件,此時HttpContext.User中就包含多張證件。所以授權策略里包含多種身份驗證方案。
一次授權可能需要多種判斷,比如同時判斷所屬角色、並且是否包含哪種類型的Calim並且.....,某些判斷可能需要對比“授權依據”,所以一個授權策略包含多個授權依據。
另外我們可以將多個授權策略合併成一個對嗎?所有的身份驗證方案合併,所有的“授權依據”合併
將來授權檢查時將根據身份驗證方案獲取當前用戶的多個證件(裡面包含很多Cliam可以用作許可權判斷),然後逐個判斷授權依據,若都滿足則認為授權檢查成功。
若是針對某個資源的授權,授權方法大概是這樣定義的xxxx.Authorize(策略,訂單),這裡不一定直接傳入整個訂單,可能只傳入訂單金額,這個根據業務需要。若是簡單的情況只判斷頁面訪問許可權,則xxx.Authorize(策略),因為當前頁面可以直接通過當前請求獲取。
在asp.net core 3.x中啟動階段我們可以定義一個授權策略列表,這個看成是全局授權策略,一直存在於應用中。
在應用運行時,每次進行授權時會動態創建一個授權策略,這個策略是最終進行本次授權檢查用的,它可能會引用某一個或多個全局策略,所謂的引用就是合併其“身份驗證方案”列表和“授權依據列表”,當然其自身的“身份驗證方案”列表和“授權依據列表”也是可以定製的,待會在AuthorizeAttribute部分再詳細說
策略構造器AuthorizationPolicyBuilder
主要用來幫助創建一個授權策略(.net中典型的Builder模式),使用步驟是:
- new一個AuthorizationPolicyBuilder
- 調用各種方法對策略進行配置
- 最後調用Build方法生成最終的授權策略。
下麵用偽代碼感受下
var builder = new AuthorizationPolicyBuilder(); builder.RequireRole("Manager","Admin"); //builder....繼續配置 var authorizationPolicy = builder.Build();
RequireRole將為最終會生成的策略中的“授權依據”列表加入一個RolesAuthorizationRequirement("Manager","Admin")。其它類似的api就不介紹了。
授權處理器AuthorizationHandler
上面說的當前用戶、授權依據、以及授權時傳遞進來的資源都是可以看成是靜態的數據,作為授權判斷的依據,真正授權的邏輯就是用IAuthorizationHandler來表示的,先看看它的定義
public interface IAuthorizationHandler { Task HandleAsync(AuthorizationHandlerContext context); }
AuthorizationHandlerContext
中包含當前用戶、授權依據列表和參與授權判斷的資源,前者是根據授權策略中的多個身份驗證方案經過身份驗證後得到的;後者就是授權策略中的授權依據列表。在方法內部處理成功或失敗的結果是直接存儲到context對象上的。
一個應用中可以存在多個AuthorizationHandler,在執行授權檢查時逐個調用它們進行檢查,若都成功則本次授權成功。
針對特定授權依據類型 的 授權處理器AuthorizationHandler<TRequirement>
上面聊過授權依據是有多種類型的,將來還可能自定義,通常授權依據不同,授權的判斷邏輯也不同。
- 比如RolesAuthorizationRequirement這個授權依據,它裡面包含角色列表,授權判斷邏輯應該是判斷當前用戶是否屬於這裡面的角色;
- 再比如OperationAuthorizationRequirement它裡面定義了操作的名稱,所以授權判斷邏輯應該是拿當前用戶以及它所屬角色和這個操作(比如是“新增”)拿到資料庫去做對比
所以這樣看來一種“授權依據”類型應該對應一種“授權處理器”,所以微軟定義了public abstract class AuthorizationHandler<TRequirement> : IAuthorizationHandler ,這個TRequirement就代表這個授權處理器類型是針對哪種類型的“授權依據的”
一個授權策略AuthorizationPolicy是包含多個“授權依據”的,這其中可能有幾個“授權依據”的類型是一樣的,只是裡面存儲的值不同,以OperationAuthorizationRequirement為例,一個授權策略里可能包含如下授權依據列表:
new OperationAuthorizationRequirement{ Name="新增" } new OperationAuthorizationRequirement{ Name="審核" } new RolesAuthorizationRequirement("Manager","Admin"); //其它。。。
所以一個授權處理器AuthorizationHandler雖然只關聯一種類型“授權依據”,但是一個授權處理器實例可以處理多個相同類型的“授權依據”
在授權過程中,每個AuthorizationHandler<TRequirement>會找到自己能處理的“授權依據”,逐個進行檢查
針對特定授權依據類型、特定類型的資源 的 授權處理器AuthorizationHandler<TRequirement, TResource>
定義是這樣的 public abstract class AuthorizationHandler<TRequirement, TResource> : IAuthorizationHandler
跟AuthorizationHandler<TRequirement>定義及處理邏輯唯一的區別是多了個TResource,在授權過程中是可以對給定資源進行判斷的,資源在AuthorizationHandlerContext.Resource,這個是object類型,為了方便子類降重重寫,所以由這裡的父類將AuthorizationHandlerContext.Resource轉換為TResource
乾脆貼下源碼吧
1 public abstract class AuthorizationHandler<TRequirement> : IAuthorizationHandler 2 where TRequirement : IAuthorizationRequirement 3 { 4 public virtual async Task HandleAsync(AuthorizationHandlerContext context) 5 { 6 foreach (var req in context.Requirements.OfType<TRequirement>()) 7 { 8 await HandleRequirementAsync(context, req); 9 } 10 } 11 12 protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement); 13 } 14 15 public abstract class AuthorizationHandler<TRequirement, TResource> : IAuthorizationHandler 16 where TRequirement : IAuthorizationRequirement 17 { 18 public virtual async Task HandleAsync(AuthorizationHandlerContext context) 19 { 20 if (context.Resource is TResource) 21 { 22 foreach (var req in context.Requirements.OfType<TRequirement>()) 23 { 24 await HandleRequirementAsync(context, req, (TResource)context.Resource); 25 } 26 } 27 } 28 29 protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, TResource resource); 30 }View Code
合併AuthorizationHandler & AuthorizationRequirement
我們發現通常一個授權依據的類型會有個對應的授權處理器,如果只定義一個類,實現這兩種介面事情不是更簡單嗎?舉個例子:
1 public class RolesAuthorizationRequirement : AuthorizationHandler<RolesAuthorizationRequirement>, IAuthorizationRequirement 2 { 3 public RolesAuthorizationRequirement(IEnumerable<string> allowedRoles) 4 { 5 AllowedRoles = allowedRoles; 6 } 7 public IEnumerable<string> AllowedRoles { get; } 8 protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement) 9 { 10 if (context.User != null) 11 { 12 bool found = false; 13 if (requirement.AllowedRoles == null || !requirement.AllowedRoles.Any()) 14 { 15 // Review: What do we want to do here? No roles requested is auto success? 16 } 17 else 18 { 19 found = requirement.AllowedRoles.Any(r => context.User.IsInRole(r)); 20 } 21 if (found) 22 { 23 context.Succeed(requirement); 24 } 25 } 26 return Task.CompletedTask; 27 } 28 }View Code
我們上面講的微軟定義的那幾個授權依據基本都是這樣定義的
何時實施授權檢查?
如果是用的asp.net core 3.x之前的版本,那麼在Action執行前做授權判斷比較合適,常用的就是過濾器Filter咯。這個我不是特別確定,至少在.net framework時代是用的授權過濾器AuthorizeAttribute
請求 > 其它中間件 > 路由中間件 > 身份驗證中間件 > MVC中間件 > Controller > [授權過濾器]Action
若是asp.net core 3.x之後,由於目前用的終結點路由,所以在 路由中間件 和 身份驗證中間件 後做許可權判斷(使用授權中間件)比較合適,因為 路由中間件執行後我們可以從當前請求上下文中獲取當前終結點(它代表一個Action或一個頁面)。身份驗證中間件執行後可以通過HttpContext.User獲取當前用戶,此時有了訪問的頁面和當前用戶 就可以做許可權判斷了
請求 > 其它中間件 > 路由中間件(這裡就拿到終結點了) > 身份驗證中間件 > 授權中間件 > MVC中間件 > Controller > Action
還有一種情況是在業務代碼內部去執行許可權判斷,比如:希望銷售訂單金額大於10000的,必須要經理角色才可以審核,此時因為我們要先獲取訂單才知道它的金額,所以我們最好在Action執行內部根據路由拿到訂單號,去資料庫查詢訂單金額後,調用某個方法執行許可權判斷。
授權服務AuthorizationService
所以執行許可權判斷的點不同,AuthorizationService就是用來封裝授權檢查的,我們在不同的點都可以來調用它執行許可權判斷。看看介面定義
public interface IAuthorizationService { Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements); Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName); }
user:要進行判斷的用戶,它裡面可能存在一張或多張證件
resource:可能是一個終結點,也可能是一個頁面RazorPage,也可能是一個訂單(或者是單獨的訂單金額)
requirements:授權依據列表
policyName:一個授權策略名
在授權中間件和在業務邏輯代碼中手動進行授權檢查時都是調用此介面
它內部會去調用AuthorizationHandler來進行許可權判斷。
定製授權依據AuthorizeAttribute : IAuthorizeData
在asp.net core 3.x中 啟動階段可以配置一個全局策略列表,它一直存在於系統中,暫時稱為“靜態策略列表”
在每次執行授權檢查時也需要一個策略,暫時稱為“運行時授權策略”,授權中間件執行時就會創建此策略,然後調用AuthorizationService根據此策略進行許可權判斷,那此策略中的“授權依據”和“身份驗證方案”這倆列表從哪來的呢?就是在Action通過AuthorizeAttribute來定製的,它實現 IAuthorizeData介面
如果你對早期版本mvc有一丟丟瞭解的話,你可能記得有個授權過濾器的概念AuthorizeAttribute,在Action執行前會先去做授權判斷,若成功才會繼續執行Action,否則就返回403.
在asp.net core 3.x中不是這樣了,AuthorizeAttribute只是用來定製當前授權策略(AuthorizationPolicy)的,並不是過濾器,它實現IAuthorizeData介面,此介面定義如下:
public interface IAuthorizeData { string Policy { get; set; }//直接指定此Action將來授權驗證時要使用的授權策略AuthorizationPolicy,此策略會被合併到當前授權策略 string Roles { get; set; } //它會創建一個基於角色的授權依據RolesAuthorizationRequirement,此依據會被放入當前授權策略 string AuthenticationSchemes { get; set; }//它用來定義當前授權策略里要使用哪些身份驗證方案 }
Policy屬性指明從“靜態策略列表”拿到指定策略,然後將其“授權依據”和“身份驗證方案”這倆列表合併到“運行時授權策略”
看個例子:
1 [Authorize(AuthenticationSchemes = "cookie,jwtBearer")] 2 [Authorize(Roles = "manager,admin")] 3 [Authorize(policy:"test")] 4 [Authorize] 5 public IActionResult Privacy() 6 { 7 return View(); 8 }
以上定製只是針對使用授權中間件來做許可權判斷時,對當前授權策略進行定製。若我們直接在業務代碼中調用AuthorizationService手動進行許可權判斷呢,就截止調用咯。參考上面的描述
授權中間件AuthorizationMiddleware
上面我們介紹了何時實施授權檢查,授權中間件(AuthorizationMiddleware)就是其中最為常用的一個授權檢查點,相當於是一個授權檢查的入口方法,它在進入MVC中間件之前就可以做授權判斷,所以比之前的在Action上做判斷更早。並且由於授權檢查是根據終結點的,因此同一套授權代碼可以應用在mvc/webapi/razorPages...等多種web框架。由於授權檢查依賴當前訪問的終結點(若不理解終結點,可以暫時認為它=Action及其之上應用的各種Attribute) 和 當前登錄用戶,因此 授權中間件 應該在 路由中間件 和 身份驗證中間件 之後註冊
1 public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
2 {
3 //略....
4 app.UseHttpsRedirection();
5 app.UseStaticFiles();
6 app.UseRouting();
7 app.UseAuthentication();
8 app.UseAuthorization();
9 app.UseEndpoints(endpoints =>
10 {
11 endpoints.MapRazorPages();
12 });
13 }
它的核心步驟大致如下:
- 從當前請求拿到終結點
- 通過終結點拿到其關聯的IAuthorizeData集合
- 通過IAuthorizeData集合創建一個複合型授權策略
- 遍歷策略中的身份驗證方案獲取多張證件,最後合併放入HttpContext.User中
- 若Action上應用了IAllowAnonymous,則放棄授權檢查(為毛不早點做這步?)
- 調用IAuthorizationService執行授權檢查
- 若授檢查結果為質詢,則遍歷策略所有的身份驗證方案,進行質詢,若策略里木有身份驗證方案則使用預設身份驗證方案進行質詢
- 若授權評估拒絕就直接調用身份驗證方案進行拒絕
所以重點是可以在執行mvc中間件之前拿到終結點及其之上定義的AuthorizeAttribute,從其中的數據就可以構建出本次許可權判斷的“授權策略”,有了授權策略就可以通過AuthorizationService執行授權判斷,內部會使用到授權處理器AuthorizationHandler
結束
暫時就BB到這裡,先有個大概印象,下一篇按asp.net core的預設授權流程走走源碼,再結合此篇應該就差不多了...