在現代應用程式中,認證已不再是簡單的將用戶憑證保存在瀏覽器中,而要適應多種場景,如App,WebAPI,第三方登錄等等。在 ASP.NET 4.x 時代的Windows認證和Forms認證已無法滿足現代化的需求,因此在ASP.NET Core 中對認證及授權進行了全新設計,使其更加靈活,可以應付各種 ...
在現代應用程式中,認證已不再是簡單的將用戶憑證保存在瀏覽器中,而要適應多種場景,如App,WebAPI,第三方登錄等等。在 ASP.NET 4.x 時代的Windows認證和Forms認證已無法滿足現代化的需求,因此在ASP.NET Core 中對認證及授權進行了全新設計,使其更加靈活,可以應付各種場景。在上一章中,我們提到HttpContext中認證相關的功能放在了獨立的模塊中,以擴展的方式來展現,以保證HttpContext的簡潔性,本章就來介紹一下 ASP.NET Core 認證系統的整個輪廓,以及它的切入點。
目錄
本系列文章從源碼分析的角度來探索 ASP.NET Core 的運行原理,分為以下幾個章節:
ASP.NET Core 運行原理解剖[1]:Hosting
ASP.NET Core 運行原理解剖[2]:Hosting補充之配置介紹
ASP.NET Core 運行原理解剖[3]:Middleware-請求管道的構成
ASP.NET Core 運行原理解剖[4]:進入HttpContext的世界
ASP.NET Core 運行原理解剖[5]:Authentication(Current)
- AuthenticationHttpContextExtensions
- IAuthenticationSchemeProvider
- IAuthenticationHandlerProvider
- IAuthenticationService
- Usage
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的註冊和查詢,定義如下:
public interface IAuthenticationSchemeProvider
{
void AddScheme(AuthenticationScheme scheme);
Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync();
Task<AuthenticationScheme> GetSchemeAsync(string name);
Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync();
Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync();
Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync();
Task<AuthenticationScheme> GetDefaultForbidSchemeAsync();
Task<AuthenticationScheme> GetDefaultSignInSchemeAsync();
Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync();
}
其 AddScheme
方法,用來註冊Scheme,而每一種Scheme最終體現為一個 AuthenticationScheme
類型的對象:
public class AuthenticationScheme
{
public AuthenticationScheme(string name, string displayName, Type handlerType)
{
if (!typeof(IAuthenticationHandler).IsAssignableFrom(handlerType))
{
throw new ArgumentException("handlerType must implement IAuthenticationSchemeHandler.");
}
...
}
public string Name { get; }
public string DisplayName { get; }
public Type HandlerType { get; }
}
每一個Scheme中還包含一個對應的IAuthenticationHandler
類型的Handler,由它來完成具體的處理邏輯,看一下它的預設實現:
public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider
{
private IDictionary<string, AuthenticationScheme> _map = new Dictionary<string, AuthenticationScheme>(StringComparer.Ordinal);
public AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options)
{
_options = options.Value;
foreach (var builder in _options.Schemes)
{
var scheme = builder.Build();
AddScheme(scheme);
}
}
private Task<AuthenticationScheme> GetDefaultSchemeAsync()
=> _options.DefaultScheme != null
? GetSchemeAsync(_options.DefaultScheme)
: Task.FromResult<AuthenticationScheme>(null);
....
}
如上,通過一個內部的字典來保存我們所註冊的Scheme,key為Scheme名稱,然後提供一系列對該字典的查詢。它還提供了一系列的GetDefaultXXXSchemeAsync
方法,所使用的Key是通過構造函數中接收的AuthenticationOptions
對象來獲取的,如果未配置,則返回為null
。
對於 AuthenticationOptions
對象,大家可能會比較熟悉,在上面介紹的 AddAuthenticationCore
擴展方法中,也是使用該對象來配置認證系統:
public class AuthenticationOptions
{
private readonly IList<AuthenticationSchemeBuilder> _schemes = new List<AuthenticationSchemeBuilder>();
public IEnumerable<AuthenticationSchemeBuilder> Schemes => _schemes;
public IDictionary<string, AuthenticationSchemeBuilder> SchemeMap { get; } = new Dictionary<string, AuthenticationSchemeBuilder>(StringComparer.Ordinal);
public void AddScheme(string name, Action<AuthenticationSchemeBuilder> configureBuilder)
{
if (SchemeMap.ContainsKey(name))
{
throw new InvalidOperationException("Scheme already exists: " + name);
}
var builder = new AuthenticationSchemeBuilder(name);
configureBuilder(builder);
_schemes.Add(builder);
SchemeMap[name] = builder;
}
public void AddScheme<THandler>(string name, string displayName) where THandler : IAuthenticationHandler
=> AddScheme(name, b =>
{
b.DisplayName = displayName;
b.HandlerType = typeof(THandler);
});
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; }
}
該對象可以幫助我們更加方便的註冊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);
}
AuthenticationHandler的創建是通過 IAuthenticationHandlerProvider
來完成的:
public interface IAuthenticationHandlerProvider
{
Task<IAuthenticationHandler> GetHandlerAsync(HttpContext context, string authenticationScheme);
}
Provider 只定義了一個 GetHandlerAsync
方法,來獲取指定的Scheme的Hander,在 ASP.NET Core 中,很多地方都使用了類似的 Provider 模式。
而HandlerProvider的實現,我們通過對上面SchemeProvider的瞭解,應該可以猜到一二,因為在 AuthenticationScheme
中已經包含了Hander:
public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider
{
public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes)
{
Schemes = schemes;
}
public IAuthenticationSchemeProvider Schemes { get; }
private Dictionary<string, IAuthenticationHandler> _handlerMap = new Dictionary<string, IAuthenticationHandler>(StringComparer.Ordinal);
public async Task<IAuthenticationHandler> GetHandlerAsync(HttpContext context, string authenticationScheme)
{
if (_handlerMap.ContainsKey(authenticationScheme))
{
return _handlerMap[authenticationScheme];
}
var scheme = await Schemes.GetSchemeAsync(authenticationScheme);
if (scheme == null)
{
return null;
}
var handler = (context.RequestServices.GetService(scheme.HandlerType) ??
ActivatorUtilities.CreateInstance(context.RequestServices, scheme.HandlerType))
as IAuthenticationHandler;
if (handler != null)
{
await handler.InitializeAsync(scheme, context);
_handlerMap[authenticationScheme] = handler;
}
return handler;
}
}
可以看到,AuthenticationHandlerProvider
首先使用 IAuthenticationSchemeProvider
獲取到當前Scheme,然後先從DI中查找是否有此Scheme中的Handler,如果未註冊到DI系統中,則使用 ActivatorUtilities
來創建其實例,並緩存到內部的 _handlerMap
字典中。
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中的同名方法:
public class AuthenticationService : IAuthenticationService
{
public IAuthenticationSchemeProvider Schemes { get; }
public IAuthenticationHandlerProvider Handlers { get; }
public IClaimsTransformation Transform { get; }
public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme)
{
if (scheme == null)
{
var defaultScheme = await Schemes.GetDefaultAuthenticateSchemeAsync();
scheme = defaultScheme?.Name;
if (scheme == null)
{
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found.");
}
}
var handler = await Handlers.GetHandlerAsync(context, scheme);
var result = await handler.AuthenticateAsync();
if (result != null && result.Succeeded)
{
var transformed = await Transform.TransformAsync(result.Principal);
return AuthenticateResult.Success(new AuthenticationTicket(transformed, result.Properties, result.Ticket.AuthenticationScheme));
}
return result;
}
}
AuthenticationService中對這5個方法的實現大致相同,首先會在我們傳入的scheme為null
時,來獲取我們所註冊的預設scheme,然後獲取調用相應Handler的即可。針對 SignInAsync
和 SignOutAsync
的實現則會判斷Handler是否實現了對應的介面,若未實現則拋出異常。
不過在這裡還涉及到如下兩個對象:
AuthenticateResult
AuthenticateResult 用來表示認證的結果:
public class AuthenticateResult
{
public AuthenticationTicket Ticket { get; protected set; }
public bool Succeeded => Ticket != null;
public ClaimsPrincipal Principal => Ticket?.Principal;
public AuthenticationProperties Properties => Ticket?.Properties;
public Exception Failure { get; protected set; }
public bool None { get; protected set; }
public static AuthenticateResult Success(AuthenticationTicket ticket) => new AuthenticateResult() { Ticket = ticket };
public static AuthenticateResult NoResult() => new AuthenticateResult() { None = true };
public static AuthenticateResult Fail(Exception failure) => new AuthenticateResult() { Failure = failure };
public static AuthenticateResult Fail(string failureMessage) => new AuthenticateResult() { Failure = new Exception(failureMessage) };
}
它主要包含一個核心屬性 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
屬性用來表示證書頒發的相關信息,如頒發時間,過期時間,重定向地址等等:
public class AuthenticationProperties
{
public IDictionary<string, string> Items { get; }
public string RedirectUri
{
get
{
string value;
return Items.TryGetValue(RedirectUriKey, out value) ? value : null;
}
set
{
if (value != null) Items[RedirectUriKey] = value;
else
{
if (Items.ContainsKey(RedirectUriKey)) Items.Remove(RedirectUriKey);
}
}
}
...
}
在上面最開始介紹的HttpContext中的 GetTokenAsync
擴展方法便是對AuthenticationProperties的擴展:
public static class AuthenticationTokenExtensions
{
private static string TokenNamesKey = ".TokenNames";
private static string TokenKeyPrefix = ".Token.";
public static void StoreTokens(this AuthenticationProperties properties, IEnumerable<AuthenticationToken> tokens) {}
public static bool UpdateTokenValue(this AuthenticationProperties properties, string tokenName, string tokenValue) {}
public static IEnumerable<AuthenticationToken> GetTokens(this AuthenticationProperties properties) { }
public static string GetTokenValue(this AuthenticationProperties properties, string tokenName)
{
var tokenKey = TokenKeyPrefix + tokenName;
return properties.Items.ContainsKey(tokenKey) ? properties.Items[tokenKey] : null;
}
public static Task<string> GetTokenAsync(this IAuthenticationService auth, HttpContext context, string tokenName)
=> auth.GetTokenAsync(context, scheme: null, tokenName: tokenName);
public static async Task<string> GetTokenAsync(this IAuthenticationService auth, HttpContext context, string scheme, string tokenName)
{
var result = await auth.AuthenticateAsync(context, scheme);
return result?.Properties?.GetTokenValue(tokenName);
}
}
如上,Token擴展只是對AuthenticationProperties中的 Items
屬性進行添加和讀取。
IClaimsTransformation
IClaimsTransformation 用來對由我們的應用程式傳入的 ClaimsPrincipal
進行轉換,它只定義了一個 Transform
方法:
public interface IClaimsTransformation
{
Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal);
}
其預設實現,不做任何處理,直接返回。它適合於全局的為 ClaimsPrincipal
添加一些預定義的聲明,如添加當前時間等,然後在DI中把我們的實現註冊進去即可。
Usage
下麵我們演示一下 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的運行方式。