快速理解ASP.NET Core的认证与授权
ASP.NET Core的認證與授權已經不是什么新鮮事了,微軟官方的文檔對于如何在ASP.NET Core中實現認證與授權有著非常詳細深入的介紹。但有時候在開發過程中,我們也往往會感覺無從下手,或者由于一開始沒有進行認證授權機制的設計與規劃,使得后期出現一些混亂的情況。這里我就嘗試結合一個實際的例子,從0到1來介紹ASP.NET Core中如何實現自己的認證與授權機制。
當我們使用Visual Studio自帶的ASP.NET Core Web API項目模板新建一個項目的時候,Visual Studio會問我們是否需要啟用認證機制,如果你選擇了啟用,那么Visual Studio會在項目創建的時候,加入一些輔助依賴和一些輔助類,比如加入對Entity Framework以及ASP.NET Identity的依賴,以幫助你實現基于Entity Framework和ASP.NET Identity的身份認證。如果你還沒有了解過ASP.NET Core的認證與授權的一些基礎內容,那么當你打開這個由Visual Studio自動創建的項目的時候,肯定會一頭霧水,不知從何開始,你甚至會懷疑自動創建的項目中,真的是所有的類或者方法都是必須的嗎?所以,為了讓本文更加簡單易懂,我們還是選擇不啟用身份認證,直接創建一個最簡單的ASP.NET Core Web API應用程序,以便后續的介紹。
新建一個ASP.NET Core Web API應用程序,這里我是在Linux下使用JetBrains Rider新建的項目,也可以使用標準的Visual Studio或者VSCode來創建項目。創建完成后,運行程序,然后使用瀏覽器訪問/WeatherForecast端點,就可以獲得一組隨機生成的天氣及溫度數據的數組。你也可以使用下面的curl命令來訪問這個API:
1 | curl -X GET "http://localhost:5000/WeatherForecast" -H? "accept: text/plain" |
現在讓我們在WeatherForecastController的Get方法上設置一個斷點,重新啟動程序,仍然發送上述請求以命中斷點,此時我們比較關心User對象的狀態,打開監視器查看User對象的屬性,發現它的IsAuthenticated屬性為false:
在很多情況下,我們可能并不需要在Controller的方法中獲取認證用戶的信息,因此也從來不會關注User對象是否真的處于已被認證的狀態。但是當API需要根據用戶的某些信息來執行一些特殊邏輯時,我們就需要在這里讓User的認證信息處于一種合理的狀態:它是已被認證的,并且包含API所需的信息。這就是本文所要討論的ASP.NET Core的認證與授權。
認證
應用程序對于使用者的身份認定包含兩部分:認證和授權。認證是指當前用戶是否是系統的合法用戶,而授權則是指定合法用戶對于哪些系統資源具有怎樣的訪問權限。我們先來看如何實現認證。
在此,我們單說由ASP.NET Core應用程序本身實現的認證,不討論具有統一Identity Provider完成身份認證的情況(比如單點登錄),這樣的話就能夠更加清晰地了解ASP.NET Core本身的認證機制。接下來,我們嘗試在ASP.NET Core應用程序上,實現Basic認證。
Basic認證需要將用戶的認證信息附屬在HTTP請求的Authorization的頭(Header)上,認證信息是一串由用戶名和密碼通過BASE64編碼后所產生的字符串,例如,當你采用Basic認證,并使用daxnet和password作為訪問WeatherForecast API的用戶名和密碼時,你可能需要使用下面的命令行來調用WeatherForecast:
1 | curl -X GET "http://localhost:5000/WeatherForecast" -H? "accept: text/plain" -H "Authorization: Basic ZGF4bmV0OnBhc3N3b3Jk" |
在ASP.NET Core Web API中,當應用程序接收到上述請求后,就會從Request的Header里讀取Authorization的信息,然后BASE64解碼得到用戶名和密碼,然后訪問數據庫來確認所提供的用戶名和密碼是否合法,以判斷認證是否成功。這部分工作通常可以采用ASP.NET Core Identity框架來實現,不過在這里,為了能夠更加清晰地了解認證的整個過程,我們選擇自己動手來實現。
首先,我們定義一個User對象,并且預先設計好幾個用戶,以便模擬存儲用戶信息的數據庫,這個User對象的代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class User { ????public string UserName { get; set; } ????public string Password { get; set; } ????public IEnumerable<string> Roles { get; set; } ????public int Age { get; set; } ????public override string ToString() => UserName; ????public static readonly User[] AllUsers = { ????????new User ????????{ ????????????UserName = "daxnet", Password = "password", Age = 16, Roles = new[] { "admin", "super_admin" } ????????}, ????????new User ????????{ ????????????UserName = "admin", Password = "admin", Age = 29, Roles = new[] { "admin" } ????????} ????}; } |
該User對象包括用戶名、密碼以及它的角色名稱,不過暫時我們不需要關心角色信息。User對象還包含一個靜態字段,我們將它作為用戶信息數據庫來使用。
接下來,在應用程序中添加一個AuthenticationHandler,用來獲取Request Header中的用戶信息,并對用戶信息進行驗證,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationSchemeOptions> { ????public BasicAuthenticationHandler( ????????IOptionsMonitor<BasicAuthenticationSchemeOptions> options, ????????ILoggerFactory logger, ????????UrlEncoder encoder, ????????ISystemClock clock) : base(options, logger, encoder, clock) ????{ ????} ????protected override Task<AuthenticateResult> HandleAuthenticateAsync() ????{ ????????if (!Request.Headers.ContainsKey("Authorization")) ????????{ ????????????return Task.FromResult(AuthenticateResult.Fail("Authorization header is not specified.")); ????????} ????????var authHeader = Request.Headers["Authorization"].ToString(); ????????if (!authHeader.StartsWith("Basic ")) ????????{ ????????????return Task.FromResult( ????????????????AuthenticateResult.Fail("Authorization header value is not in a correct format")); ????????} ????????var base64EncodedValue = authHeader["Basic ".Length..]; ????????var userNamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(base64EncodedValue)); ????????var userName = userNamePassword.Split(':')[0]; ????????var password = userNamePassword.Split(':')[1]; ????????var user = User.AllUsers.FirstOrDefault(u => u.UserName == userName && u.Password == password); ????????if (user == null) ????????{ ????????????return Task.FromResult(AuthenticateResult.Fail("Invalid username or password.")); ????????} ????????var claims = new[] ????????{ ????????????new Claim(ClaimTypes.NameIdentifier, user.UserName), ????????????new Claim(ClaimTypes.Role, string.Join(',', user.Roles)), ????????????new Claim(ClaimTypes.UserData, user.Age.ToString()) ????????}; ????????var claimsPrincipal = ????????????new ClaimsPrincipal(new ClaimsIdentity( ????????????????claims, ????????????????"Basic", ????????????????ClaimTypes.NameIdentifier, ClaimTypes.Role)); ????????var ticket = new AuthenticationTicket(claimsPrincipal, new AuthenticationProperties ????????{ ????????????IsPersistent = false ????????}, "Basic"); ????????return Task.FromResult(AuthenticateResult.Success(ticket)); ????} } |
在上面的HandleAuthenticateAsync代碼中,首先對Request Header進行合法性校驗,比如是否包含Authorization的Header,以及Authorization Header的值是否合法,然后,將Authorization Header的值解析出來,通過Base64解碼后得到用戶名和密碼,與用戶信息數據庫里的記錄進行匹配,找到匹配的用戶。接下來,基于找到的用戶對象,創建ClaimsPrincipal,并基于ClaimsPrincipal創建AuthenticationTicket然后返回。
這段代碼中有幾點值得關注:
BasicAuthenticationSchemeOptions本身只是一個繼承于AuthenticationSchemeOptions的POCO類。AuthenticationSchemeOptions類通常是為了向AuthenticationHandler提供一些輸入參數。比如,在某個自定義的用戶認證邏輯中,可能需要通過環境變量讀入字符串解密的密鑰信息,此時就可以在這個自定義的AuthenticationSchemeOptions中增加一個Passphrase的屬性,然后在Startup.cs中,通過service.AddScheme調用將從環境變量中讀取的Passphrase的值傳入
除了將用戶名作為Identity Claim加入到ClaimsPrincipal中之外,我們還將用戶的角色(Role)用逗號串聯起來,作為Role Claim添加到ClaimsPrincipal中,目前我們暫時不需要涉及角色相關的內容,但是先將這部分代碼放在這里以備后用。另外,我們將用戶的年齡(Age)放在UserData claim中,在實際中應該是在用戶對象上有該用戶的出生日期,這樣比較合理,然后這個出生日期應該放在DateOfBirth claim中,這里為了簡單起見,就先放在UserData中了
ClaimsPrincipal的構造函數中,可以指定哪個Claim類型可被用作用戶名稱,而哪個Claim類型又可被用作用戶的角色。例如上面代碼中,我們選擇NameIdentifier類型作為用戶名,而Role類型作為用戶角色,于是,在接下來的Controller代碼中,由NameIdentifier這種Claim所指向的字符串值,就會被看成用戶名而被綁定到Identity.Name屬性上
回過頭來看看BasicAuthenticationSchemeOptions類,它的實現非常簡單:
1 2 3 4 | public class BasicAuthenticationSchemeOptions : AuthenticationSchemeOptions { } |
接下來,在Startup.cs文件里,修改ConfigureServices和Configure方法,加入Authentication的支持:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public void ConfigureServices(IServiceCollection services) { ????services.AddControllers(); ????services.AddSwaggerGen(c => ????{ ????????c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebAPIAuthSample", Version = "v1" }); ????}); ????services.AddAuthentication("Basic") ????????.AddScheme<BasicAuthenticationSchemeOptions, BasicAuthenticationHandler>( ????????????"Basic", options => { }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { ????if (env.IsDevelopment()) ????{ ????????app.UseDeveloperExceptionPage(); ????????app.UseSwagger(); ????????app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebAPIAuthSample v1")); ????} ????app.UseHttpsRedirection(); ????app.UseRouting(); ????app.UseAuthentication(); ????app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } |
現在,運行應用程序,在WeatherForecastController的Get方法上設置斷點,然后執行上面的curl命令,當斷點被命中時,觀察this.User對象可以發現,IsAuthenticated屬性變為了true,Name屬性也被設置為用戶名:
大多數身份認證框架會提供一些輔助方法來幫助開發人員將AuthenticationHandler注冊到應用程序中,例如,基于JWT持有者身份認證的框架會提供一個AddJwtBearer的方法,將JWT身份認證機制加入到應用程序中,它本質上也是調用AddScheme方法來完成AuthenticationHandler的注冊。在這里,我們也可以自定義一個AddBasicAuthentication的擴展方法:
1 2 3 4 5 6 7 | public static class Extensions { ????public static AuthenticationBuilder AddBasicAuthentication(this AuthenticationBuilder builder) ????????=> builder.AddScheme<BasicAuthenticationSchemeOptions, BasicAuthenticationHandler>( ????????????"Basic", ????????????options => { }); } |
然后修改Starup.cs文件,將ConfigureServices方法改為下面這個樣子:
1 2 3 4 5 6 7 8 9 | public void ConfigureServices(IServiceCollection services) { ????services.AddControllers(); ????services.AddSwaggerGen(c => ????{ ????????c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebAPIAuthSample", Version = "v1" }); ????}); ????services.AddAuthentication("Basic").AddBasicAuthentication(); } |
這樣做的好處是,你可以為開發人員提供更多比較有針對性的配置認證機制的編程接口,這對于一個認證模塊/框架的開發是一個很好的設計。
在curl命令中,如果我們沒有指定Authorization Header,或者Authorization Header的值不正確,那么WeatherForecast API仍然可以被調用,只不過IsAuthenticated屬性為false,也無法從this.User對象得到用戶信息。其實,阻止未認證用戶訪問API并不是認證的事情,API被未認證(或者說未登錄)用戶訪問也是合理的事情,因此,要實現對于未認證用戶的訪問限制,就需要進一步實現ASP.NET Core Web API的另一個安全控制組件:授權。
授權
與認證相比,授權的邏輯會比較復雜:認證更多是技術層面的事情,而授權則更多地與業務相關。市面上常見的認證機制頂多也就是那么幾種或者十幾種,而授權的方式則是多樣化的,因為不同app不同業務,對于app資源訪問的授權需求是不同的。最為常見的一種授權方式就是RBAC(Role Based Access Control,基于角色的訪問控制),它定義了什么樣的角色對于什么資源具有怎樣的訪問權限。在RBAC中,不同的用戶都被賦予了不同的角色,而為了管理方便,又為具有相同資源訪問權限的用戶設計了用戶組,而將訪問控制設置在用戶組上,更進一步,組和組之間還可以有父子關系。
請注意上面的黑體字,每一個黑體標注的詞語都是授權相關的概念,在ASP.NET Core中,每一個授權需求(Authorization Requirement)對應一個實現IAuthorizationRequirement的類,并由AuthorizationHandler負責處理相應的授權邏輯。簡單地理解,授權需求表示什么樣的用戶才能夠滿足被授權的要求,或者說什么樣的用戶才能夠通過授權去訪問資源。一個授權需求往往僅定義并處理一種特定的授權邏輯,ASP.NET Core允許將多個授權需求組合成授權策略(Authorization Policy)然后應用到被訪問的資源上,這樣的設計可以保證授權需求的設計與實現都是小粒度的,從而分離不同授權需求的關注點。在授權策略的層面,通過組合不同授權需求從而達到靈活實現授權業務的目的。
比如:假設app中有的API只允許管理員訪問,而有的API只允許滿18周歲的用戶訪問,而另外的一些API需要用戶既是超級管理員又滿18歲。那么就可以定義兩種Authorization Requirement:GreaterThan18Requirement和SuperAdminRequirement,然后設計三種Policy:第一種只包含GreaterThan18Requirement,第二種只包含SuperAdminRequirement,第三種則同時包含這兩種Requirement,最后將這些不同的Policy應用到不同的API上就可以了。
回到我們的案例代碼,首先定義兩個Requirement:SuperAdminRequirement和GreaterThan18Requirement:
1 2 3 4 5 6 | public class SuperAdminRequirement : IAuthorizationRequirement { } public class GreaterThan18Requirement : IAuthorizationRequirement { } |
然后分別實現SuperAdminAuthorizationHandle和GreaterThan18AuthorizationHandler:
實現邏輯也非常清晰:在GreaterThan18AuthorizationHandler中,通過UserData claim獲得年齡信息,如果年齡大于18,則授權成功;在SuperAdminAuthorizationHandler中,通過Role claim獲得用戶所處的角色,如果角色中包含super_admin,則授權成功。接下來就需要將這兩個Requirement加到所需的Policy中,然后注冊到應用程序里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | public void ConfigureServices(IServiceCollection services) { ????services.AddControllers(); ????services.AddSwaggerGen(c => ????{ ????????c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebAPIAuthSample", Version = "v1" }); ????}); ????services.AddAuthentication("Basic").AddBasicAuthentication(); ????services.AddAuthorization(options => ????{ ????????options.AddPolicy("AgeMustBeGreaterThan18", builder => ????????{ ????????????builder.Requirements.Add(new GreaterThan18Requirement()); ????????}); ????????options.AddPolicy("UserMustBeSuperAdmin", builder => ????????{ ????????????builder.Requirements.Add(new SuperAdminRequirement()); ????????}); ????}); ????services.AddSingleton<IAuthorizationHandler, GreaterThan18AuthorizationHandler>(); ????services.AddSingleton<IAuthorizationHandler, SuperAdminAuthorizationHandler>(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { ????if (env.IsDevelopment()) ????{ ????????app.UseDeveloperExceptionPage(); ????????app.UseSwagger(); ????????app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebAPIAuthSample v1")); ????} ????app.UseHttpsRedirection(); ????app.UseRouting(); ????app.UseAuthentication(); ????app.UseAuthorization(); ????app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } |
在ConfigureServices方法中,我們定義了兩種Policy:AgeMustBeGreaterThan18和UserMustBeSuperAdmin,最后,在API Controller或者Action上,應用AuthorizeAttribute,從而指定所需的Policy即可。比如,如果希望WeatherForecase API只有年齡大于18歲的用戶才能訪問,那么就可以這樣做:
1 2 3 4 5 6 7 8 9 10 11 12 13 | [HttpGet] [Authorize(Policy = "AgeMustBeGreaterThan18")] public IEnumerable<WeatherForecast> Get() { ????var rng = new Random(); ????return Enumerable.Range(1, 5).Select(index => new WeatherForecast ????????{ ????????????Date = DateTime.Now.AddDays(index), ????????????TemperatureC = rng.Next(-20, 55), ????????????Summary = Summaries[rng.Next(Summaries.Length)] ????????}) ????????.ToArray(); } |
運行程序,假設有三個用戶:daxnet、admin和foo,它們的BASE64認證信息分別為:
daxnet:ZGF4bmV0OnBhc3N3b3Jk
admin:YWRtaW46YWRtaW4=
foo:Zm9vOmJhcg==
那么,相同的curl命令,指定不同的用戶認證信息時,得到的結果是不一樣的:
daxnet用戶年齡小于18歲,所以訪問API不成功,服務端返回403:
admin用戶滿足年齡大于18歲的條件,所以可以成功訪問API:
而foo用戶本身沒有在系統中注冊,所以服務端返回401,表示用戶沒有認證成功:
小結
本文簡要介紹了ASP.NET Core中用戶身份認證與授權的基本實現方法,幫助初學者或者需要使用這些功能的開發人員快速理解這部分內容。ASP.NET Core的認證與授權體系非常靈活,能夠集成各種不同的認證機制與授權方式,文章也無法進行全面詳細的介紹。不過無論何種框架哪種實現,它的實現基礎也就是本文所介紹的這些內容,如果打算自己開發一套認證和授權的框架,也可以參考本文。
總結
以上是生活随笔為你收集整理的快速理解ASP.NET Core的认证与授权的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数字化架构
- 下一篇: 推荐:Flowchart 一种通过文本方