快速理解ASP.NET Core的认证与授权
ASP.NET Core的認(rèn)證與授權(quán)已經(jīng)不是什么新鮮事了,微軟官方的文檔對于如何在ASP.NET Core中實現(xiàn)認(rèn)證與授權(quán)有著非常詳細(xì)深入的介紹。但有時候在開發(fā)過程中,我們也往往會感覺無從下手,或者由于一開始沒有進(jìn)行認(rèn)證授權(quán)機制的設(shè)計與規(guī)劃,使得后期出現(xiàn)一些混亂的情況。這里我就嘗試結(jié)合一個實際的例子,從0到1來介紹ASP.NET Core中如何實現(xiàn)自己的認(rèn)證與授權(quán)機制。
當(dāng)我們使用Visual Studio自帶的ASP.NET Core Web API項目模板新建一個項目的時候,Visual Studio會問我們是否需要啟用認(rèn)證機制,如果你選擇了啟用,那么Visual Studio會在項目創(chuàng)建的時候,加入一些輔助依賴和一些輔助類,比如加入對Entity Framework以及ASP.NET Identity的依賴,以幫助你實現(xiàn)基于Entity Framework和ASP.NET Identity的身份認(rèn)證。如果你還沒有了解過ASP.NET Core的認(rèn)證與授權(quán)的一些基礎(chǔ)內(nèi)容,那么當(dāng)你打開這個由Visual Studio自動創(chuàng)建的項目的時候,肯定會一頭霧水,不知從何開始,你甚至?xí)岩勺詣觿?chuàng)建的項目中,真的是所有的類或者方法都是必須的嗎?所以,為了讓本文更加簡單易懂,我們還是選擇不啟用身份認(rèn)證,直接創(chuàng)建一個最簡單的ASP.NET Core Web API應(yīng)用程序,以便后續(xù)的介紹。
新建一個ASP.NET Core Web API應(yīng)用程序,這里我是在Linux下使用JetBrains Rider新建的項目,也可以使用標(biāo)準(zhǔn)的Visual Studio或者VSCode來創(chuàng)建項目。創(chuàng)建完成后,運行程序,然后使用瀏覽器訪問/WeatherForecast端點,就可以獲得一組隨機生成的天氣及溫度數(shù)據(jù)的數(shù)組。你也可以使用下面的curl命令來訪問這個API:
1 | curl -X GET "http://localhost:5000/WeatherForecast" -H? "accept: text/plain" |
現(xiàn)在讓我們在WeatherForecastController的Get方法上設(shè)置一個斷點,重新啟動程序,仍然發(fā)送上述請求以命中斷點,此時我們比較關(guān)心User對象的狀態(tài),打開監(jiān)視器查看User對象的屬性,發(fā)現(xiàn)它的IsAuthenticated屬性為false:
在很多情況下,我們可能并不需要在Controller的方法中獲取認(rèn)證用戶的信息,因此也從來不會關(guān)注User對象是否真的處于已被認(rèn)證的狀態(tài)。但是當(dāng)API需要根據(jù)用戶的某些信息來執(zhí)行一些特殊邏輯時,我們就需要在這里讓User的認(rèn)證信息處于一種合理的狀態(tài):它是已被認(rèn)證的,并且包含API所需的信息。這就是本文所要討論的ASP.NET Core的認(rèn)證與授權(quán)。
認(rèn)證
應(yīng)用程序?qū)τ谑褂谜叩纳矸菡J(rèn)定包含兩部分:認(rèn)證和授權(quán)。認(rèn)證是指當(dāng)前用戶是否是系統(tǒng)的合法用戶,而授權(quán)則是指定合法用戶對于哪些系統(tǒng)資源具有怎樣的訪問權(quán)限。我們先來看如何實現(xiàn)認(rèn)證。
在此,我們單說由ASP.NET Core應(yīng)用程序本身實現(xiàn)的認(rèn)證,不討論具有統(tǒng)一Identity Provider完成身份認(rèn)證的情況(比如單點登錄),這樣的話就能夠更加清晰地了解ASP.NET Core本身的認(rèn)證機制。接下來,我們嘗試在ASP.NET Core應(yīng)用程序上,實現(xiàn)Basic認(rèn)證。
Basic認(rèn)證需要將用戶的認(rèn)證信息附屬在HTTP請求的Authorization的頭(Header)上,認(rèn)證信息是一串由用戶名和密碼通過BASE64編碼后所產(chǎn)生的字符串,例如,當(dāng)你采用Basic認(rèn)證,并使用daxnet和password作為訪問WeatherForecast API的用戶名和密碼時,你可能需要使用下面的命令行來調(diào)用WeatherForecast:
1 | curl -X GET "http://localhost:5000/WeatherForecast" -H? "accept: text/plain" -H "Authorization: Basic ZGF4bmV0OnBhc3N3b3Jk" |
在ASP.NET Core Web API中,當(dāng)應(yīng)用程序接收到上述請求后,就會從Request的Header里讀取Authorization的信息,然后BASE64解碼得到用戶名和密碼,然后訪問數(shù)據(jù)庫來確認(rèn)所提供的用戶名和密碼是否合法,以判斷認(rèn)證是否成功。這部分工作通常可以采用ASP.NET Core Identity框架來實現(xiàn),不過在這里,為了能夠更加清晰地了解認(rèn)證的整個過程,我們選擇自己動手來實現(xiàn)。
首先,我們定義一個User對象,并且預(yù)先設(shè)計好幾個用戶,以便模擬存儲用戶信息的數(shù)據(jù)庫,這個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對象包括用戶名、密碼以及它的角色名稱,不過暫時我們不需要關(guān)心角色信息。User對象還包含一個靜態(tài)字段,我們將它作為用戶信息數(shù)據(jù)庫來使用。
接下來,在應(yīng)用程序中添加一個AuthenticationHandler,用來獲取Request Header中的用戶信息,并對用戶信息進(jìn)行驗證,代碼如下:
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進(jìn)行合法性校驗,比如是否包含Authorization的Header,以及Authorization Header的值是否合法,然后,將Authorization Header的值解析出來,通過Base64解碼后得到用戶名和密碼,與用戶信息數(shù)據(jù)庫里的記錄進(jìn)行匹配,找到匹配的用戶。接下來,基于找到的用戶對象,創(chuàng)建ClaimsPrincipal,并基于ClaimsPrincipal創(chuàng)建AuthenticationTicket然后返回。
這段代碼中有幾點值得關(guān)注:
BasicAuthenticationSchemeOptions本身只是一個繼承于AuthenticationSchemeOptions的POCO類。AuthenticationSchemeOptions類通常是為了向AuthenticationHandler提供一些輸入?yún)?shù)。比如,在某個自定義的用戶認(rèn)證邏輯中,可能需要通過環(huán)境變量讀入字符串解密的密鑰信息,此時就可以在這個自定義的AuthenticationSchemeOptions中增加一個Passphrase的屬性,然后在Startup.cs中,通過service.AddScheme調(diào)用將從環(huán)境變量中讀取的Passphrase的值傳入
除了將用戶名作為Identity Claim加入到ClaimsPrincipal中之外,我們還將用戶的角色(Role)用逗號串聯(lián)起來,作為Role Claim添加到ClaimsPrincipal中,目前我們暫時不需要涉及角色相關(guān)的內(nèi)容,但是先將這部分代碼放在這里以備后用。另外,我們將用戶的年齡(Age)放在UserData claim中,在實際中應(yīng)該是在用戶對象上有該用戶的出生日期,這樣比較合理,然后這個出生日期應(yīng)該放在DateOfBirth claim中,這里為了簡單起見,就先放在UserData中了
ClaimsPrincipal的構(gòu)造函數(shù)中,可以指定哪個Claim類型可被用作用戶名稱,而哪個Claim類型又可被用作用戶的角色。例如上面代碼中,我們選擇NameIdentifier類型作為用戶名,而Role類型作為用戶角色,于是,在接下來的Controller代碼中,由NameIdentifier這種Claim所指向的字符串值,就會被看成用戶名而被綁定到Identity.Name屬性上
回過頭來看看BasicAuthenticationSchemeOptions類,它的實現(xiàn)非常簡單:
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(); }); } |
現(xiàn)在,運行應(yīng)用程序,在WeatherForecastController的Get方法上設(shè)置斷點,然后執(zhí)行上面的curl命令,當(dāng)斷點被命中時,觀察this.User對象可以發(fā)現(xiàn),IsAuthenticated屬性變?yōu)榱藅rue,Name屬性也被設(shè)置為用戶名:
大多數(shù)身份認(rèn)證框架會提供一些輔助方法來幫助開發(fā)人員將AuthenticationHandler注冊到應(yīng)用程序中,例如,基于JWT持有者身份認(rèn)證的框架會提供一個AddJwtBearer的方法,將JWT身份認(rèn)證機制加入到應(yīng)用程序中,它本質(zhì)上也是調(diào)用AddScheme方法來完成AuthenticationHandler的注冊。在這里,我們也可以自定義一個AddBasicAuthentication的擴(kuò)展方法:
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(); } |
這樣做的好處是,你可以為開發(fā)人員提供更多比較有針對性的配置認(rèn)證機制的編程接口,這對于一個認(rèn)證模塊/框架的開發(fā)是一個很好的設(shè)計。
在curl命令中,如果我們沒有指定Authorization Header,或者Authorization Header的值不正確,那么WeatherForecast API仍然可以被調(diào)用,只不過IsAuthenticated屬性為false,也無法從this.User對象得到用戶信息。其實,阻止未認(rèn)證用戶訪問API并不是認(rèn)證的事情,API被未認(rèn)證(或者說未登錄)用戶訪問也是合理的事情,因此,要實現(xiàn)對于未認(rèn)證用戶的訪問限制,就需要進(jìn)一步實現(xiàn)ASP.NET Core Web API的另一個安全控制組件:授權(quán)。
授權(quán)
與認(rèn)證相比,授權(quán)的邏輯會比較復(fù)雜:認(rèn)證更多是技術(shù)層面的事情,而授權(quán)則更多地與業(yè)務(wù)相關(guān)。市面上常見的認(rèn)證機制頂多也就是那么幾種或者十幾種,而授權(quán)的方式則是多樣化的,因為不同app不同業(yè)務(wù),對于app資源訪問的授權(quán)需求是不同的。最為常見的一種授權(quán)方式就是RBAC(Role Based Access Control,基于角色的訪問控制),它定義了什么樣的角色對于什么資源具有怎樣的訪問權(quán)限。在RBAC中,不同的用戶都被賦予了不同的角色,而為了管理方便,又為具有相同資源訪問權(quán)限的用戶設(shè)計了用戶組,而將訪問控制設(shè)置在用戶組上,更進(jìn)一步,組和組之間還可以有父子關(guān)系。
請注意上面的黑體字,每一個黑體標(biāo)注的詞語都是授權(quán)相關(guān)的概念,在ASP.NET Core中,每一個授權(quán)需求(Authorization Requirement)對應(yīng)一個實現(xiàn)IAuthorizationRequirement的類,并由AuthorizationHandler負(fù)責(zé)處理相應(yīng)的授權(quán)邏輯。簡單地理解,授權(quán)需求表示什么樣的用戶才能夠滿足被授權(quán)的要求,或者說什么樣的用戶才能夠通過授權(quán)去訪問資源。一個授權(quán)需求往往僅定義并處理一種特定的授權(quán)邏輯,ASP.NET Core允許將多個授權(quán)需求組合成授權(quán)策略(Authorization Policy)然后應(yīng)用到被訪問的資源上,這樣的設(shè)計可以保證授權(quán)需求的設(shè)計與實現(xiàn)都是小粒度的,從而分離不同授權(quán)需求的關(guān)注點。在授權(quán)策略的層面,通過組合不同授權(quán)需求從而達(dá)到靈活實現(xiàn)授權(quán)業(yè)務(wù)的目的。
比如:假設(shè)app中有的API只允許管理員訪問,而有的API只允許滿18周歲的用戶訪問,而另外的一些API需要用戶既是超級管理員又滿18歲。那么就可以定義兩種Authorization Requirement:GreaterThan18Requirement和SuperAdminRequirement,然后設(shè)計三種Policy:第一種只包含GreaterThan18Requirement,第二種只包含SuperAdminRequirement,第三種則同時包含這兩種Requirement,最后將這些不同的Policy應(yīng)用到不同的API上就可以了。
回到我們的案例代碼,首先定義兩個Requirement:SuperAdminRequirement和GreaterThan18Requirement:
1 2 3 4 5 6 | public class SuperAdminRequirement : IAuthorizationRequirement { } public class GreaterThan18Requirement : IAuthorizationRequirement { } |
然后分別實現(xiàn)SuperAdminAuthorizationHandle和GreaterThan18AuthorizationHandler:
實現(xiàn)邏輯也非常清晰:在GreaterThan18AuthorizationHandler中,通過UserData claim獲得年齡信息,如果年齡大于18,則授權(quán)成功;在SuperAdminAuthorizationHandler中,通過Role claim獲得用戶所處的角色,如果角色中包含super_admin,則授權(quán)成功。接下來就需要將這兩個Requirement加到所需的Policy中,然后注冊到應(yīng)用程序里:
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上,應(yīng)用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(); } |
運行程序,假設(shè)有三個用戶:daxnet、admin和foo,它們的BASE64認(rèn)證信息分別為:
daxnet:ZGF4bmV0OnBhc3N3b3Jk
admin:YWRtaW46YWRtaW4=
foo:Zm9vOmJhcg==
那么,相同的curl命令,指定不同的用戶認(rèn)證信息時,得到的結(jié)果是不一樣的:
daxnet用戶年齡小于18歲,所以訪問API不成功,服務(wù)端返回403:
admin用戶滿足年齡大于18歲的條件,所以可以成功訪問API:
而foo用戶本身沒有在系統(tǒng)中注冊,所以服務(wù)端返回401,表示用戶沒有認(rèn)證成功:
小結(jié)
本文簡要介紹了ASP.NET Core中用戶身份認(rèn)證與授權(quán)的基本實現(xiàn)方法,幫助初學(xué)者或者需要使用這些功能的開發(fā)人員快速理解這部分內(nèi)容。ASP.NET Core的認(rèn)證與授權(quán)體系非常靈活,能夠集成各種不同的認(rèn)證機制與授權(quán)方式,文章也無法進(jìn)行全面詳細(xì)的介紹。不過無論何種框架哪種實現(xiàn),它的實現(xiàn)基礎(chǔ)也就是本文所介紹的這些內(nèi)容,如果打算自己開發(fā)一套認(rèn)證和授權(quán)的框架,也可以參考本文。
總結(jié)
以上是生活随笔為你收集整理的快速理解ASP.NET Core的认证与授权的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数字化架构
- 下一篇: .NET 6 中的七个 System.T