在现代应用程序中,认证已不再是简单的将用户凭证保存在浏览器中,而要适应多种场景,如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 DbContextservices.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的运行方式。
相关文章:
.NET Core 2.0 正式发布信息汇总
.NET Standard 2.0 特性介绍和使用指南
.NET Core 2.0 的dll实时更新、https、依赖包变更问题及解决
.NET Core 2.0 特性介绍和使用指南
Entity Framework Core 2.0 新特性
体验 PHP under .NET Core
.NET Core 2.0使用NLog
升级项目到.NET Core 2.0,在Linux上安装Docker,并成功部署
解决Visual Studio For Mac Restore失败的问题
ASP.NET Core 2.0 特性介绍和使用指南
.Net Core下通过Proxy 模式 使用 WCF
.NET Core 2.0 开源Office组件 NPOI
ASP.NET Core Razor页面 vs MVC
Razor Page–Asp.Net Core 2.0新功能 Razor Page介绍
MySql 使用 EF Core 2.0 CodeFirst、DbFirst、数据库迁移(Migration)介绍及示例
.NET Core 2.0迁移技巧之web.config配置文件
asp.net core MVC 过滤器之ExceptionFilter过滤器(一)
ASP.NET Core 使用Cookie验证身份
ASP.NET Core MVC – Tag Helpers 介绍
ASP.NET Core MVC – Caching Tag Helpers
ASP.NET Core MVC – Form Tag Helpers
ASP.NET Core MVC – 自定义 Tag Helpers
ASP.NET Core MVC – Tag Helper 组件
ASP.NET Core 运行原理解剖[1]:Hosting
ASP.NET Core 运行原理解剖[2]:Hosting补充之配置介绍
ASP.NET Core 运行原理解剖[3]:Middleware-请求管道的构成
ASP.NET Core 运行原理解剖[4]:进入HttpContext的世界
原文地址:http://www.cnblogs.com/RainingNight/p/authentication-in-asp-net-core.html
.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注