在现代应用程序中,认证已不再是简单的将用户凭证保存在浏览器中,而要适应多种场景,如App,WebAPI,第三方登录等等。在 ASP.NET 4.x 时代的Windows认证和Forms认证已无法满足现代化的需求,因此在ASP.NET Core 中对认证及授权进行了全新设计,使其更加灵活,可以应付各种场景。在中,我们提到HttpContext中认证相关的功能放在了独立的模块中,以扩展的方式来展现,以保证HttpContext的简洁性,本章就来介绍一下 ASP.NET Core 认证系统的整个轮廓,以及它的切入点。
目录
本系列文章从源码分析的角度来探索 ASP.NET Core 的运行原理,分为以下几个章节:
ASP.NET Core 运行原理解剖[5]:Authentication(Current)
AuthenticationHttpContextExtensions
AuthenticationHttpContextExtensions 类是对 HttpContext 认证相关的扩展,它提供了如下扩展方法:
public static class AuthenticationHttpContextExtensions{ public static TaskAuthenticateAsync(this HttpContext context, string scheme) => context.RequestServices.GetRequiredService ().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 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(); services.TryAddSingleton (); // Can be replaced with scoped ones that use DbContext services.TryAddScoped (); services.TryAddSingleton (); return services; } public static IServiceCollection AddAuthenticationCore(this IServiceCollection services, Action 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> GetAllSchemesAsync(); Task GetSchemeAsync(string name); Task > GetRequestHandlerSchemesAsync(); Task GetDefaultAuthenticateSchemeAsync(); Task GetDefaultChallengeSchemeAsync(); Task GetDefaultForbidSchemeAsync(); Task GetDefaultSignInSchemeAsync(); Task 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_map = new Dictionary (StringComparer.Ordinal); public AuthenticationSchemeProvider(IOptions options) { _options = options.Value; foreach (var builder in _options.Schemes) { var scheme = builder.Build(); AddScheme(scheme); } } private Task GetDefaultSchemeAsync() => _options.DefaultScheme != null ? GetSchemeAsync(_options.DefaultScheme) : Task.FromResult (null); ....}
如上,通过一个内部的字典来保存我们所注册的Scheme,key为Scheme名称,然后提供一系列对该字典的查询。它还提供了一系列的GetDefaultXXXSchemeAsync
方法,所使用的Key是通过构造函数中接收的AuthenticationOptions
对象来获取的,如果未配置,则返回为null
。
对于 AuthenticationOptions
对象,大家可能会比较熟悉,在上面介绍的 AddAuthenticationCore
扩展方法中,也是使用该对象来配置认证系统:
public class AuthenticationOptions{ private readonly IList_schemes = new List (); public IEnumerable Schemes => _schemes; public IDictionary SchemeMap { get; } = new Dictionary (StringComparer.Ordinal); public void AddScheme(string name, Action 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 (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); TaskAuthenticateAsync(); Task ChallengeAsync(AuthenticationProperties properties); Task ForbidAsync(AuthenticationProperties properties);}
AuthenticationHandler的创建是通过 IAuthenticationHandlerProvider
来完成的:
public interface IAuthenticationHandlerProvider{ TaskGetHandlerAsync(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_handlerMap = new Dictionary (StringComparer.Ordinal); public async Task 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{ TaskAuthenticateAsync(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 TaskAuthenticateAsync(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 IDictionaryItems { 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, IEnumerabletokens) {} public static bool UpdateTokenValue(this AuthenticationProperties properties, string tokenName, string tokenValue) {} public static IEnumerable 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 GetTokenAsync(this IAuthenticationService auth, HttpContext context, string tokenName) => auth.GetTokenAsync(context, scheme: null, tokenName: tokenName); public static async Task 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{ TaskTransformAsync(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 TaskAuthenticateAsync() { 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("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 认证系统的基本用法,当然,在实际使用中要比这更加复杂,如安全性,易用性等方面的完善,但本质上也就这么多东西。
总结
本章基于 对 ASP.NET Core 认证系统做了一个简单的介绍,但大多是一些抽象层次的定义,并未涉及到具体的实现。因为现实中有各种各样的场景无法预测,HttpAbstractions 提供了统一的认证规范,在我们的应用程序中,可以根据具体需求来灵活的扩展适合的认证方式。不过在 提供了更加具体的实现方式,也包含了 Cookie, JwtBearer, OAuth, OpenIdConnect 等较为常用的认证实现。在下个系列会来详细介绍一下 ASP.NET Core 的认证与授权,更加偏向于实战,敬请期待!
ASP.NET Core 在GitHub上的开源地址为:,包含了100多个项目,ASP.NET Core 的核心是 HttpAbstractions ,其它的都是围绕着 HttpAbstractions 进行的扩展。本系列文章所涉及到的源码只包含 和 ,它们两个已经构成了一个完整的 ASP.NET Core 运行时,不需要其它模块,就可以轻松应对一些简单的场景。当然,更多的时候我们还会使用比较熟悉的 来大大提高开发速度和体验,后续再来介绍一下MVC的运行方式。