ASP.NET Core [5]: Authentication(筆記)
原文連接:http://www.cnblogs.com/RainingNight/p/authentication-in-asp-net-core.html
在現代應用程序中,認證已不再是簡單的將用戶憑證保存在瀏覽器中,而要適應多種場景,如App,WebAPI,第三方登錄等等。在 ASP.NET 4.x 時代的Windows認證和Forms認證已無法滿足現代化的需求,因此在ASP.NET Core 中對認證及授權進行了全新設計,使其更加靈活,可以應付各種場景。在上一章中,我們提到HttpContext中認證相關的功能放在了獨立的模塊中,以擴展的方式來展現,以保證HttpContext的簡潔性,本章就來介紹一下 ASP.NET Core 認證系統的整個輪廓,以及它的切入點。
AuthenticationHttpContextExtensions
AuthenticationHttpContextExtensions 類是對 HttpContext 認證相關的擴展,它提供了如下擴展方法:
public static class AuthenticationHttpContextExtensions { public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) => context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, scheme); public static Task ChallengeAsync(this HttpContext context, string scheme, AuthenticationProperties properties) { } public static Task ForbidAsync(this HttpContext context, string scheme, AuthenticationProperties properties) { }public static Task SignInAsync(this HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) {} public static Task SignOutAsync(this HttpContext context, string scheme, AuthenticationProperties properties) { } public static Task<string> GetTokenAsync(this HttpContext context, string scheme, string tokenName) { } }
主要包括如上6個擴展方法,其它的只是一些參數重載:
-
SignInAsync 用戶登錄成功後頒發一個證書(加密的用戶憑證),用來標識用戶的身份。
-
SignOutAsync 退出登錄,如清除Coookie等。
-
AuthenticateAsync 驗證在
SignInAsync
中頒發的證書,並返回一個AuthenticateResult
對象,表示用戶的身份。 -
ChallengeAsync 返回一個需要認證的標識來提示用戶登錄,通常會返回一個
401
狀態碼。 -
ForbidAsync 禁上訪問,表示用戶權限不足,通常會返回一個
403
狀態碼。 -
GetTokenAsync 用來獲取
AuthenticationProperties
中保存的額外信息。
它們的實現都非常簡單,與展示的第一個方法類似,從DI系統中獲取到 IAuthenticationService
接口實例,然後調用其同名方法。
因此,如果我們希望使用認證服務,那麽首先要註冊 IAuthenticationService
的實例,ASP.NET Core 中也提供了對應註冊擴展方法
public static class AuthenticationCoreServiceCollectionExtensions { public static IServiceCollection AddAuthenticationCore(this IServiceCollection services) { services.TryAddScoped<IAuthenticationService, AuthenticationService>(); services.TryAddSingleton<IClaimsTransformation, NoopClaimsTransformation>(); // Can be replaced with scoped ones that use DbContext services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>(); services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>(); return services; } public static IServiceCollection AddAuthenticationCore(this IServiceCollection services, Action<AuthenticationOptions> configureOptions) { services.AddAuthenticationCore(); services.Configure(configureOptions); return services; } }
如上,AddAuthenticationCore 中註冊了認證系統的三大核心對象:IAuthenticationSchemeProvider
,IAuthenticationHandlerProvider
和 IAuthenticationService
,以及一個對Claim進行轉換的 IClaimsTransformation(不常用),下面就來介紹一下這三大對象。
IAuthenticationSchemeProvider
首先來解釋一下 Scheme 是用來做什麽的。因為在 ASP.NET Core 中可以支持各種各樣的認證方式(如,cookie, bearer, oauth, openid 等等),而 Scheme 用來標識使用的是哪種認證方式,不同的認證方式其處理方式是完全不一樣的,所以Scheme是非常重要的。
IAuthenticationSchemeProvider 用來提供對Scheme的註冊和查詢,其 AddScheme
方法,用來註冊Scheme,而每一種Scheme最終體現為一個 AuthenticationScheme
類型的對象: 每一個Scheme中還包含一個對應的IAuthenticationHandler
類型的Handler,由它來完成具體的處理邏輯,看一下它的默認實現:
對於 AuthenticationOptions
對象,大家可能會比較熟悉,在上面介紹的 AddAuthenticationCore
擴展方法中,也是使用該對象來配置認證系統:
public class AuthenticationOptions { public AuthenticationOptions(); public IEnumerable<AuthenticationSchemeBuilder> Schemes { get; } public IDictionary<string, AuthenticationSchemeBuilder> SchemeMap { get; } public string DefaultScheme { get; set; } public string DefaultAuthenticateScheme { get; set; } public string DefaultSignInScheme { get; set; } public string DefaultSignOutScheme { get; set; } public string DefaultChallengeScheme { get; set; } public string DefaultForbidScheme { get; set; } public void AddScheme(string name, Action<AuthenticationSchemeBuilder> configureBuilder); public void AddScheme<THandler>(string name, string displayName) where THandler : IAuthenticationHandler; }
services.AddAuthentication(option =>{ option.DefaultAuthenticateScheme=JwtBearerDefaults.AuthenticationScheme; option.DefaultChallengeScheme=JwtBearerDefaults.AuthenticationScheme; })
該對象可以幫助我們更加方便的註冊Scheme,提供泛型和 AuthenticationSchemeBuilder
兩種方式配置方式。
到此,我們了解到,要想使用認證系統,必要先註冊Scheme,而每一個Scheme必須指定一個Handler,否則會拋出異常,下面我們就來了解一下Handler。
IAuthenticationHandlerProvider
在 ASP.NET Core 的認證系統中,AuthenticationHandler 負責對用戶憑證的驗證,它定義了如下接口:
public interface IAuthenticationHandler { Task InitializeAsync(AuthenticationScheme scheme, HttpContext context); Task<AuthenticateResult> AuthenticateAsync(); Task ChallengeAsync(AuthenticationProperties properties); Task ForbidAsync(AuthenticationProperties properties); }
IAuthenticationService
IAuthenticationService 本質上是對 IAuthenticationSchemeProvider 和 IAuthenticationHandlerProvider 封裝,用來對外提供一個統一的認證服務接口:
public interface IAuthenticationService { Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme); Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties); Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties); Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties); Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties); }
這5個方法中,都需要接收一個 scheme
參數,因為只有先指定你要使用的認證方式,才能知道該如何進行認證。
對於上面的前三個方法,我們知道在IAuthenticationHandler中都有對應的實現,而SignInAsync
和SignOutAsync
則使用了獨立的定義接口:
public interface IAuthenticationSignInHandler : IAuthenticationSignOutHandler { Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties); } public interface IAuthenticationSignOutHandler : IAuthenticationHandler { Task SignOutAsync(AuthenticationProperties properties); }
SignInAsync 和 SignOutAsync 之所以使用獨立的接口,是因為在現代架構中,通常會提供一個統一的認證中心,負責證書的頒發及銷毀(登入和登出),而其它服務只用來驗證證書,並用不到SingIn/SingOut。
而 IAuthenticationService 的默認實現 AuthenticationService 中的邏輯就非常簡單了,只是調用Handler中的同名方法:
AuthenticationService中對這5個方法的實現大致相同,首先會在我們傳入的scheme為null
時,來獲取我們所註冊的默認scheme,然後獲取調用相應Handler的即可。針對 SignInAsync
和 SignOutAsync
的實現則會判斷Handler是否實現了對應的接口,若未實現則拋出異常。
AuthenticateResult
AuthenticateResult 用來表示認證的結果:
它主要包含一個核心屬性 AuthenticationTicket
:
public class AuthenticationTicket { public string AuthenticationScheme { get; private set; } public ClaimsPrincipal Principal { get; private set; } public AuthenticationProperties Properties { get; private set; } }
我們可以把AuthenticationTicket看成是一個經過認證後頒發的證書,
其 ClaimsPrincipal
屬性我們較為熟悉,表示證書的主體,在基於聲明的認證中,用來標識一個人的身份(如:姓名,郵箱等等),後續會詳細介紹一下基於聲明的認證。
而 AuthenticationProperties
屬性用來表示證書頒發的相關信息,如頒發時間,過期時間,重定向地址等等: 在上面最開始介紹的HttpContext中的 GetTokenAsync
擴展方法便是對AuthenticationProperties的擴展: oken擴展只是對AuthenticationProperties中的 Items
屬性進行添加和讀取。
下面我們演示一下 ASP.NET Core 認證系統的實際用法:
首先,我們要定義一個Handler:
public class MyHandler : IAuthenticationHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler { public AuthenticationScheme Scheme { get; private set; } protected HttpContext Context { get; private set; } public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) { Scheme = scheme; Context = context; return Task.CompletedTask; } public async Task<AuthenticateResult> AuthenticateAsync() { var cookie = Context.Request.Cookies["mycookie"]; if (string.IsNullOrEmpty(cookie)) { return AuthenticateResult.NoResult(); } return AuthenticateResult.Success(Deserialize(cookie)); } public Task ChallengeAsync(AuthenticationProperties properties) { Context.Response.Redirect("/login"); return Task.CompletedTask; } public Task ForbidAsync(AuthenticationProperties properties) { Context.Response.StatusCode = 403; return Task.CompletedTask; } public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) { var ticket = new AuthenticationTicket(user, properties, Scheme.Name); Context.Response.Cookies.Append("myCookie", Serialize(ticket)); return Task.CompletedTask; } public Task SignOutAsync(AuthenticationProperties properties) { Context.Response.Cookies.Delete("myCookie"); return Task.CompletedTask; } }
如上,在 SignInAsync
中將用戶的Claim序列化後保存到Cookie中,在 AuthenticateAsync
中從Cookie中讀取並反序列化成用戶Claim。
然後在DI系統中註冊我們的Handler和Scheme:
public void ConfigureServices(IServiceCollection services) { services.AddAuthenticationCore(options => options.AddScheme<MyHandler>("myScheme", "demo scheme")); }
最後,便可以通過HttpContext來調用認證系統了:
public void Configure(IApplicationBuilder app) { // 登錄 app.Map("/login", builder => builder.Use(next => { return async (context) => { var claimIdentity = new ClaimsIdentity(); claimIdentity.AddClaim(new Claim(ClaimTypes.Name, "jim")); await context.SignInAsync("myScheme", new ClaimsPrincipal(claimIdentity)); }; })); // 退出 app.Map("/logout", builder => builder.Use(next => { return async (context) => { await context.SignOutAsync("myScheme"); }; })); // 認證 app.Use(next => { return async (context) => { var result = await context.AuthenticateAsync("myScheme"); if (result?.Principal != null) context.User = result.Principal; await next(context); }; }); // 授權 app.Use(async (context, next) => { var user = context.User; if (user?.Identity?.IsAuthenticated ?? false) { if (user.Identity.Name != "jim") await context.ForbidAsync("myScheme"); else await next(); } else { await context.ChallengeAsync("myScheme"); } }); // 訪問受保護資源 app.Map("/resource", builder => builder.Run(async (context) => await context.Response.WriteAsync("Hello, ASP.NET Core!")));
在這裏完整演示了 ASP.NET Core 認證系統的基本用法,當然,在實際使用中要比這更加復雜,如安全性,易用性等方面的完善,但本質上也就這麽多東西。
總結
本章基於 HttpAbstractions 對 ASP.NET Core 認證系統做了一個簡單的介紹,但大多是一些抽象層次的定義,並未涉及到具體的實現。因為現實中有各種各樣的場景無法預測,HttpAbstractions 提供了統一的認證規範,在我們的應用程序中,可以根據具體需求來靈活的擴展適合的認證方式。不過在 Security 提供了更加具體的實現方式,也包含了 Cookie, JwtBearer, OAuth, OpenIdConnect 等較為常用的認證實現。在下個系列會來詳細介紹一下 ASP.NET Core 的認證與授權,更加偏向於實戰,敬請期待!
ASP.NET Core 在GitHub上的開源地址為:https://github.com/aspnet,包含了100多個項目,ASP.NET Core 的核心是 HttpAbstractions ,其它的都是圍繞著 HttpAbstractions 進行的擴展。本系列文章所涉及到的源碼只包含 Hosting 和 HttpAbstractions ,它們兩個已經構成了一個完整的 ASP.NET Core 運行時,不需要其它模塊,就可以輕松應對一些簡單的場景。當然,更多的時候我們還會使用比較熟悉的 Mvc 來大大提高開發速度和體驗,後續再來介紹一下MVC的運行方式。
ASP.NET Core [5]: Authentication(筆記)