原文鏈接: https://www.cnblogs.com/RainingNight/p/introduce-basic-authentication-in-asp-net-core.html
在ASP.NET 4.X 中,我們最常用的是Forms認證,它既可以用於局域網環境,也可用於互聯網環境,有着非常廣泛的使用。但是它很難進行擴展,更無法與第三方認證集成,因此,在 ASP.NET Core 中對認證與授權進行了全新的設計,並使用基於聲明的認證(claims-based authentication),以適應現代化應用的需求。在運行原理解剖[5]:Authentication中介紹了一下HttpContext與認證系統的集成,本系列文章則來詳細介紹一下 ASP.NET Core 中認證與授權。
基於聲明的認證有兩個主要的特點:
-
將認證與授權拆分成兩個獨立的服務。
-
在需要授權的服務中,不用再去關心你是如何認證的,你用Windows認證也好,Forms認證也行,只要你出示你的 Claims就行了。
ASP.NET Core 中的用戶身份
Claim
在 ASP.NET Core 中,使用Cliam
類來表示用戶身份中的一項信息,它由核心的Type
和Value
屬性構成:
一個Claim可以是“用戶的姓名”,“郵箱地址”,“電話”,等等,而多個Claim構成一個用戶的身份,使用ClaimsIdentity
類來表示:
public class ClaimsIdentity : IIdentity { public virtual IEnumerable<Claim> Claims {get;} public virtual string AuthenticationType => _authenticationType; public virtual bool IsAuthenticated => !string.IsNullOrEmpty(_authenticationType); public virtual string Name { get { Claim claim = FindFirst(_nameClaimType); if (claim != null) return claim.Value; return null; } } }
如上,其Name
屬性用來查找Claims中,第一個Type為我們創建ClaimsIdentity
時指定的NameClaimType的Claim的值,若未指定Type時則使用默認的ClaimTypes.Name
。而IsAuthenticated
只是判斷_authenticationType
是否為空,_authenticationType
則對應上一章中介紹的Scheme。
// 創建一個用戶身份,注意需要指定AuthenticationType,否則IsAuthenticated將為false。 var claimIdentity = new ClaimsIdentity("myAuthenticationType"); // 添加幾個Claim claimIdentity.AddClaim(new Claim(ClaimTypes.Name, "bob")); claimIdentity.AddClaim(new Claim(ClaimTypes.Email, "bob@gmail.com")); claimIdentity.AddClaim(new Claim(ClaimTypes.MobilePhone, "18888888888"));
如上,我們可以根據需要添加任意個的Claim,最后我們還需要再將用戶身份放到ClaimsPrincipal
對象中。
ClaimsPrincipal
ASP.NET Core 中,HttpContext直接使用的就是ClaimsPrincipal
類型
public abstract class HttpContext { public abstract ClaimsPrincipal User { get; set; } }
ClaimsPrincipal的創建非常簡單,只需傳入我們上面創建的用戶身份即可:
var principal = new ClaimsPrincipal(claimIdentity);
由於HTTP是無狀態的,我們通常使用Cookie,請求頭或請求參數等方式來附加用戶的信息,在網絡上進行傳輸,這就涉及到序列化和安全方面的問題。因此,還需要將principal
對象包裝成AuthenticationTicket
對象。
AuthenticationTicket
我們創建完ClaimsPrincipal
對象后,需要將它生成一個用戶票據並頒發給用戶,然后用戶拿着這個票據,便可以訪問受保持的資源,而在 ASP.NET Core 中,用戶票據用AuthenticationTicket
來表示,如在Cookie認證中,其認證后的Cookie值便是對該對象序列化后的結果,它的定義如下:
用戶票據除了包含上面創建的principal
對象外,還需要指定一個AuthenticationScheme
(通常在授權中用來驗證Scheme),並且還包含一個AuthenticationProperties
對象,它主要是一些用戶票據安全方面的一些配置,如過期時間,是否持久等。
var properties = new AuthenticationProperties(); var ticket = new AuthenticationTicket(principal, properties, "myScheme"); // 加密 序列化 var token = Protect(ticket);
最后,我們可以將票據(token)寫入到Cookie中,或是也可以以JSON的形式返回讓客戶端自行保存,由於我們對票據進行了加密,可以保證在網絡中安全的傳輸而不會被篡改。
最終身份令牌的結構大概是這樣的:
Microsoft.AspNetCore.Authentication
上面,我們介紹了身份票據的創建過程,下面就來介紹一下 ASP.NET Core 中的身份認證。
ASP.NET Core 中的認證系統具體實現在 Security 項目中,它包含 Cookie
, JwtBearer
, OAuth
, OpenIdConnect
等:
Usage
而對於認證系統的配置,分為兩步,也是我們所熟悉的注冊服務和配置中間件:
首先,在DI中注冊服務認證所需的服務:
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(options => { options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie() .AddOpenIdConnect(o => { o.ClientId = "server.hybrid"; o.ClientSecret = "secret"; o.Authority = "https://demo.identityserver.io/"; o.ResponseType = OpenIdConnectResponseType.CodeIdToken; }); }
最后,注冊認證中間件:
public void Configure(IApplicationBuilder app) { app.UseAuthentication(); }
如上,我們的系統便支持了Cookie
和JwtBearer
兩種認證方式
Microsoft.AspNetCore.Authentication,是所有認證實現的公共抽象類,它定義了實現認證Handler的規范,並包含一些共用的方法,如令牌加密,序列化等,AddAuthentication
便是其提供的統一的注冊認證服務的擴展方法:
public static AuthenticationBuilder AddAuthentication(this IServiceCollection services) { services.AddAuthenticationCore(); services.AddDataProtection(); services.AddWebEncoders(); services.TryAddSingleton<ISystemClock, SystemClock>(); return new AuthenticationBuilder(services); } public static AuthenticationBuilder AddAuthentication(this IServiceCollection services, Action<AuthenticationOptions> configureOptions) { var builder = services.AddAuthentication(); services.Configure(configureOptions); return builder; }
如上,它首先會調用上一章中介紹的AddAuthenticationCore
方法,然后注冊了DataProtection
和WebEncoders
兩個服務。而對 AuthenticationOptions
我們之前在IAuthenticationSchemeProvider也介紹過,它用來配置Scheme。
AddScheme
在上面的 AddAuthentication 中返回的是一個AuthenticationBuilder
類型,所有認證Handler的注冊都是以它的擴展形式來實現的,它同時也提供了AddScheme
擴展方法,使我們可以更加方便的來配置Scheme:在這里的AddScheme 擴展方法只是封裝了對AuthenticationOptions
中AddScheme
的調用,如上面示例中的AddCookie
便是調用該擴展方法來實現的。
AddRemoteScheme
看到 Remote 我們應該就可以猜到它是一種遠程驗證方式
UseAuthentication
在上面,注冊認證中間件時,我們只需調用一個UseAuthentication
擴展方法,因為它會執行我們注冊的所有認證Handler:
public static IApplicationBuilder UseAuthentication(this IApplicationBuilder app) { return app.UseMiddleware<AuthenticationMiddleware>(); }
public class AuthenticationMiddleware { private readonly RequestDelegate _next; public IAuthenticationSchemeProvider Schemes { get; set; } public async Task Invoke(HttpContext context) { context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature { OriginalPath = context.Request.Path, OriginalPathBase = context.Request.PathBase }); var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>(); foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync()) { var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler; if (handler != null && await handler.HandleRequestAsync()) { return; } } var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync(); if (defaultAuthenticate != null) { var result = await context.AuthenticateAsync(defaultAuthenticate.Name); if (result?.Principal != null) { context.User = result.Principal; } } await _next(context); } }
很簡單,但是很強大,不管我們是使用Cookie認證,還是Bearer認證,等等,都只需要這一個中間件,因為它會解析所有的Handler來執行。
不過,在這里,這會先判斷是否具體實現了IAuthenticationRequestHandler
的Hander,優先來執行,這個是什么鬼?
查了一下,發現IAuthenticationRequestHandler是在HttpAbstractions中定義的,只是在運行原理解剖[5]:Authentication中沒有介紹到它:
public interface IAuthenticationRequestHandler : IAuthenticationHandler { Task<bool> HandleRequestAsync(); }
它多了一個HandleRequestAsync
方法,那么它存在的意義是什么呢?其實在Cookie認證中並沒有用到它,它通常在遠程認證(如:OAuth, OIDC等)中使用,下文再來介紹。
繼續分析上面代碼,通過調用Schemes.GetDefaultAuthenticateSchemeAsync
來獲取到認證的Scheme,也就是上文提到的問題,我們必須指定默認的Scheme。
最后,調用AuthenticateAsync
方法進行認證,認證成功后,為HttpContext.User
賦值,至於如何解析身份令牌生成ClaimsPrincipal
對象,則交給相應的Handler來處理。
認證Handler
上文中多次提到認證Handler,它由統一的AuthenticationMiddleware
來調用,負責具體的認證實現,並分為本地認證與遠程認證兩種方式。
在本地驗證中,身份令牌的發放與認證通常是由同一個服務器來完成,這也是我們比較熟悉的場景,對於Cookie, JwtBearer等認證來說,都屬於是本地驗證。而當我們使用OAuth, OIDC等驗證方式時,身份令牌的發放則是由獨立的服務或是第三方(QQ, Weibo 等)認證來提供,此時在我們的應用程序中獲取身份令牌時需要請求遠程服務器,因此稱之為遠程驗證。
AuthenticationHandler
AuthenticationHandler是所有認證Handler的抽象基類,對於本地認證直接實現該類即可,定義如下:
public abstract class AuthenticationHandler<TOptions> : IAuthenticationHandler where TOptions : AuthenticationSchemeOptions, new() { ... public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) { ... await InitializeEventsAsync(); await InitializeHandlerAsync(); } protected virtual async Task InitializeEventsAsync() { } protected virtual Task<object> CreateEventsAsync() => Task.FromResult(new object()); protected virtual Task InitializeHandlerAsync() => Task.CompletedTask; public async Task<AuthenticateResult> AuthenticateAsync() { var result = await HandleAuthenticateOnceAsync(); ... } protected Task<AuthenticateResult> HandleAuthenticateOnceAsync() { if (_authenticateTask == null) { _authenticateTask = HandleAuthenticateAsync(); } return _authenticateTask; } protected abstract Task<AuthenticateResult> HandleAuthenticateAsync(); protected virtual Task HandleForbiddenAsync(AuthenticationProperties properties) { Response.StatusCode = 403; return Task.CompletedTask; } protected virtual Task HandleChallengeAsync(AuthenticationProperties properties) { Response.StatusCode = 401; return Task.CompletedTask; } ... }
如上,它定義一個抽象方法HandleAuthenticateAsync
,並使用HandleAuthenticateOnceAsync
方法來保證其在每次認證只執行一次。而HandleAuthenticateAsync
是認證的核心,交給具體的認證Handler負責實現。而對於 ChallengeAsync, ForbidAsync 等方法也提供了默認的實現。
而對於HandleAuthenticateAsync
的實現,大致的邏輯就是從請求中獲取上面發放的身份令牌,然后解析成AuthenticationTicket
,並經過一系列的驗證,最終返回ClaimsPrincipal對象。
RemoteAuthenticationHandler
RemoteAuthenticationHandler
便是所有遠程認證的抽象基類了,它繼承自AuthenticationHandler
,並實現了IAuthenticationRequestHandler
接口:
在上面介紹的AuthenticationMiddleware
中,提到它會先執行實現了IAuthenticationRequestHandler
接口的Handler(遠程認證),之后(若未完成認證)再執行本地認證Handler。
而RemoteAuthenticationHandler
中核心的認證邏輯便是 HandleRequestAsync
方法,它主要包含2個步驟:
-
首先執行一個抽象方法
HandleRemoteAuthenticateAsync
,由具體的Handler來實現,該方法返回的HandleRequestResult
對象包含驗證的結果(跳過,失敗,成功等),在成功時會包含一個ticket對象。 -
若上一步驗證成功,則根據返回的ticket,獲取到
ClaimsPrincipal
對象,並調用其它認證Handler的Context.SignInAsync
方法。
也就是說,遠程Hander會在用戶未登錄時,指引用戶跳轉到認證服務器,登錄成功后,解析認證服務器傳回的憑證,最終依賴於本地Handler來保存身份令牌。當用戶再次訪問則無需經過遠程Handler,直接交給本地Handler來處理。
由此也可以知道,遠程認證中本身並不具備SignIn的能力,所以必須通過指定其它SignInScheme
交給本地認證來完成 SignIn。
對於其父類的HandleAuthenticateAsync
抽象方法則定義了一個默認實現:“直接轉交給本地驗證來處理”。當我們需要定義自己的遠程認證方式時,通常只需實現 HandleRemoteAuthenticateAsync 即可,而不用再去處理 HandleAuthenticateAsync 。
總結
基於聲明的認證並不是微軟所特有的,它在國外被廣泛的使用,如微軟的ADFS,Google,Facebook,Twitter等等。在基於聲明的認證中,對認證和授權進行了明確的區分,認證用來頒發一個用戶的身份標識,其包含這個用戶的基本信息,而對於這個身份的頒發則由我們信任的第三方機構來(STS)頒發(當然,你也可以自己來頒發)。而授權,則是通過獲取身份標識中的信息,來判斷該用戶能做什么,不能做什么。
本文對 ASP.NET Core 中認證系統的整個流程做了一個簡要的介紹,可能會比較苦澀難懂,不過沒關系,大致有個印象就好,下一章則詳細介紹一下最常用的本地認證方式:Cookie認證,后續也會詳細介紹 OIDC 的用法與實現,到時再回頭來看本文或許會豁然開朗。