本文主要参考以下系列文章
https://www.cnblogs.com/RainingNight/tag/Authentication/
https://www.cnblogs.com/jionsoft/tag/身份验证/
前言
在 ASP.NET Core沿用了ASP.NET里面的Identity组件库,负责对用户的身份进行认证。ASP.NET Core提倡的是基于声明(Claim)的认证。
基于声明的认证对认证和授权进行了明确的区分,认证用来颁发一个用户的身份标识。而授权则是通过获取身份标识中的信息,来判断该用户能做什么,不能做什么。
Claims相关对象
- Claim:证件单元,存储信息最小单位
- ClaimsIdentity:相当于是一个证件
- ClaimsPrincipal:则是证件的持有者
一个ClaimsPrincipal中可以有多个ClaimsIdentity,而一个ClaimsIdentity中可以有多个Claim。
Claim:证件单元
Claim claim = new Claim(ClaimTypes.NameIdentifier, user.Code);
ClaimsIdentity:证件
public class ClaimsIdentity:IIdentity
{
public ClaimsIdentity(IEnumerable<Claim> claims){}
public virtual string Name { get; }
public string Label { get; set; }
//证件类型
public virtual string AuthenticationType { get; }
//是否是合法的证件。
bool IsAuthenticated { get; }
public virtual void AddClaim(Claim claim);
public virtual void RemoveClaim(Claim claim);
public virtual void FindClaim(Claim claim);
}
ClaimsPrincipal:证件当事人
现实生活中,一个人有多种证件,比如身份证、登机牌、就诊卡等,你去到不同的机构需要出示不同的证件,ClaimsPrincipal就代表上面说的这个人,证件用ClaimsIdentity表示,当然得有一张证件作为主要证件(如身份证)。上面我们一直拿一个人拥有多张证件来举例,其实并不准确,因为对系统来说并不关心是谁登录,可能是一个用户、也可能是一个第三方应用。所以将ClaimsPrincipal理解为一个登录到系统的主体更合理。
在一个系统中可能同时存在多种身份验证方案,比如我们系统本身做了用户管理功能,使用最简单的cookie身份验证方案,或者使用第三方登录,微信、QQ、支付宝账号登录,通常一个身份验证方案可以产生一张证件(ClaimsIdentity),当然某个身份验证方案也可以将获得的Claim添加到一张现有的证件中,这个是灵活的。默认情况下,用户登录时asp.net core会选择设置好的默认身份验证方案做身份验证,本质是创建一个ClaimsPrincipal,并根据当前请求创建一个证件(ClaimsIdentity),然后将此ClaimsIdentity加入到ClaimsPrincipal,最后将这个ClaimsPrincipal设置到HttpContext.User属性上。
当用户登录后,我们已经可以从HttpContext.User拿到当前用户,里面就包含一张或多张证件,后续的权限判断通常就依赖里面的信息,比如所属角色、所属部门,除了证件的信息我们也可以通过用户id去数据库中查询得到更多用户信息作为权限判断的依据。
public class ClaimsPrincipal:IPrincipal
{
public ClaimsPrincipal(IEnumerable<ClaimsIdentity> identities){}
//当事人的主身份证件
public virtual IIdentity Identity { get; }
public virtual IEnumerable<ClaimsIdentity> Identities { get; }
//在否属于某个角色
bool IsInRole(string role);
public virtual void AddIdentity(ClaimsIdentity identity);
}
AuthenticationHandler(身份验证处理器)
AuthenticationHandler中包含身份验证流程中核心的处理步骤,下面以cookie身份验证为例:
- SignIn:在登录时验证通过后将用户标识加密后存储到cookie
- SignOut:当用户注销时,需要清楚代表用户标识的cookie
- Authenticate:在登录时从请求中获取用户标识
- Challenge:质询/挑战,意思是当发现没有从当前请求中发现用户标识时怎么办,可能是跳转到登录页,也可能是直接响应401,或者跳转到第三方(如QQ、微信)的登录页
- Forbid:如权限验证不通过
身份验证处理器就是用来跟身份验证相关的步骤的,这些步骤在系统的不同地方来调用(比如在登录页对应的Action、在请求抵达时、在授权中间件中),
每个调用时都可以指定使用哪种身份验证方案,如果不提供将使用默认方案来做对应的操作。
不同的身份验证方式有不同的AuthenticationHandler实现
IAuthenticationHandler
IAuthenticationHandler接口只定义了最核心的几个步骤:Authenticate()、Challenge()、Forbid()。登录和注销这两个步骤定义了对应的子接口。
AuthenticationSchemeOptions(某个具体的身份验证方案的选项)
在上述身份验证处理的多个步骤中会用到一些选项数据,比如基于cookie的身份验证 cookeName、有效时长、再比如从请求时从cookie中解析得到用户标识后回调选项中的某个回调函数,允许我们的代码向调试中添加额外数据,或者干脆替换整个标识。
所以身份验证选项用来允许我们控制AuthenticationHandler的执行。不同的身份验证方式有不同的选项对象,比如:CookieAuthenticationOptions、JwtBearerOptions.. 它们直接或间接继承AuthenticationSchemeOptions
我们以cookie认证举例
CookieAuthenticationOptions就是针对这个cookie身份验证方案的选项对象
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie("换个方案名","显示名",opt=> {
opt.SlidingExpiration = true;
//其它设置...
}).AddJwtBearer();
AuthenticationOptions(整个应用的身份验证选项)
上面说的AuthenticationSchemeOptions是指某个具体身份验证方案的选项。AuthenticationOptions则是针对整个身份验证功能的选项对象,我们需要在应用启动阶段通过它来配置身份验证功能。
AuthenticationOptions对象允许我们对身份验证做整体配置,这个配置主要体现为:支持的身份验证方案列表;指定默认身份验证方案、默认登录时用的身份验证方案...默认注销...等。我们可以通过AddAuthentication(Action
services.AddAuthentication(authenticationOptions=> {
authenticationOptions.AddScheme<CookieAuthenticationHandler>("cookie", "显示名cookie");
authenticationOptions.AddScheme<JwtBearerHandler>("jwt","显示名jwtToken");
authenticationOptions.DefaultAuthenticateScheme = "cookie";
//...其它配置
});
AddScheme添加身份验证方案。
public void AddScheme(string name, Action<AuthenticationSchemeBuilder> configureBuilder)
{
var builder = new AuthenticationSchemeBuilder(name);
configureBuilder(builder);
_schemes.Add(builder);
}
name方案名;configureBuilder允许我们提供委托对方案进行配置
添加的这些方案最终会被存储到AuthenticationSchemeProvider供其使用
另外DefaultAuthenticateScheme、DefaultSignInScheme、DefaultSignOutScheme..看名字也晓得它是说当我们调用某个步骤未指定使用那个方案是的默认选择
AuthenticationScheme(身份验证方案)
总结性的说:身份验证方案 = 名称 + 身份验证处理器类型 + 选项,暂时可以理解一种身份验证方式 对应 一个身份验证方案,比如:
基于用户名密码+cookie的身份验证方式 对应的 身份验证方案为:new AuthenticationScheme("UIDPWDCookie",typeof(CookieAuthenticationHandler))
基于用户名密码+token 的身份验证方式 对应的 身份验证方案为:new AuthenticationScheme("JwtBearer",typeof(JwtBearerHandler))
身份验证方案在程序启动阶段配置,启动后形成一个身份验证方案列表。
程序运行阶段从这个列表中取出指定方案,得到对应的处理器类型,然后创建它,最后调用这个处理器做相应处理
比如登录操作的Action中xxx.SignIn("方案名") > 通过方案名找到方案从而得到对应的处理器类型 > 创建处理器 > 调用其SignIn方法
一种特殊的情况可能多种方案使用同一个身份验证处理器类型,这个后续的集成第三方登录来说
身份验证方案的容器AuthenticationSchemeProvider
它是身份验证方案的容器(Dictionary<方案名,身份验证方案>),默认是单例形式注册到依赖注入容器的
在应用启动时通过AuthenticationOptions添加的各种身份验证方案会被存储到这个容器中
各种GetDefaultXXX用来获取针对特定步骤的默认方案,如:GetDefaultAuthenticateSchemeAsync中间件从请求获取用户标识时用来获取针对此步骤的默认方案、GetDefaultSignInSchemeAsync获取默认用来登录的方案...等等,身份验证的不同步骤可以设置不同的默认方案。如果针对单独的步骤没有设置默认方案,则自动尝试获取默认方案,通过AuthenticationOptions设置这些默认值
身份验证过程中各个步骤都会通过AuthenticationSchemeProvider对象拿到指定方案,并通过关联的身份验证类型获得最终身份验证处理器,然后做相应处理
由于AuthenticationSchemeProvider的构造函数定义了IOptions
public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider
{
public AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options)
: this(options, new Dictionary<string, AuthenticationScheme>(StringComparer.Ordinal))
{
}
protected AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes)
{
_options = options.Value;
_schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
_requestHandlers = new List<AuthenticationScheme>();
foreach (var builder in _options.Schemes)
{
var scheme = builder.Build();
//options中的scheme添加到_schemes
AddScheme(scheme);
}
}
private readonly AuthenticationOptions _options;
private readonly object _lock = new object();
//已注册的scheme
private readonly IDictionary<string, AuthenticationScheme> _schemes;
//实现了IAuthenticationRequestHandler接口的Scheme
private readonly List<AuthenticationScheme> _requestHandlers;
//用作枚举api的安全返回值(防止遍历的时候删除元素抛出异常)
private IEnumerable<AuthenticationScheme> _schemesCopy = Array.Empty<AuthenticationScheme>();
private IEnumerable<AuthenticationScheme> _requestHandlersCopy = Array.Empty<AuthenticationScheme>();
private Task<AuthenticationScheme?> GetDefaultSchemeAsync()
=> _options.DefaultScheme != null
? GetSchemeAsync(_options.DefaultScheme)
: Task.FromResult<AuthenticationScheme?>(null);
public virtual Task<AuthenticationScheme?> GetDefaultAuthenticateSchemeAsync()
=> _options.DefaultAuthenticateScheme != null
? GetSchemeAsync(_options.DefaultAuthenticateScheme)
: GetDefaultSchemeAsync();
public virtual Task<AuthenticationScheme?> GetDefaultChallengeSchemeAsync()
=> _options.DefaultChallengeScheme != null
? GetSchemeAsync(_options.DefaultChallengeScheme)
: GetDefaultSchemeAsync();
public virtual Task<AuthenticationScheme?> GetDefaultForbidSchemeAsync()
=> _options.DefaultForbidScheme != null
? GetSchemeAsync(_options.DefaultForbidScheme)
: GetDefaultChallengeSchemeAsync();
public virtual Task<AuthenticationScheme?> GetDefaultSignInSchemeAsync()
=> _options.DefaultSignInScheme != null
? GetSchemeAsync(_options.DefaultSignInScheme)
: GetDefaultSchemeAsync();
public virtual Task<AuthenticationScheme?> GetDefaultSignOutSchemeAsync()
=> _options.DefaultSignOutScheme != null
? GetSchemeAsync(_options.DefaultSignOutScheme)
: GetDefaultSignInSchemeAsync();
public virtual Task<AuthenticationScheme?> GetSchemeAsync(string name)
=> Task.FromResult(_schemes.ContainsKey(name) ? _schemes[name] : null);
public virtual Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync()
=> Task.FromResult(_requestHandlersCopy);
public virtual bool TryAddScheme(AuthenticationScheme scheme)
{
if (_schemes.ContainsKey(scheme.Name))
{
return false;
}
lock (_lock)
{
if (_schemes.ContainsKey(scheme.Name))
{
return false;
}
if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType))
{
_requestHandlers.Add(scheme);
_requestHandlersCopy = _requestHandlers.ToArray();
}
_schemes[scheme.Name] = scheme;
_schemesCopy = _schemes.Values.ToArray();
return true;
}
}
public virtual void AddScheme(AuthenticationScheme scheme)
{
if (_schemes.ContainsKey(scheme.Name))
{
throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
}
lock (_lock)
{
if (!TryAddScheme(scheme))
{
throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
}
}
}
public virtual void RemoveScheme(string name)
{
if (!_schemes.ContainsKey(name))
{
return;
}
lock (_lock)
{
if (_schemes.ContainsKey(name))
{
var scheme = _schemes[name];
if (_requestHandlers.Remove(scheme))
{
_requestHandlersCopy = _requestHandlers.ToArray();
}
_schemes.Remove(name);
_schemesCopy = _schemes.Values.ToArray();
}
}
}
public virtual Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync()
=> Task.FromResult(_schemesCopy);
}
AuthenticationHandlerProvider(身份验证处理器工厂)
可以把它理解为AuthenticationHandler的运行时容器或工厂
它是以Scope的形式注册到依赖注入容器的,所以每次请求都会创建一个实例对象。
唯一方法GetHandlerAsync从AuthenticationSchemeProvider获取指定身份验证方案,然后通过方案关联的AuthenticationHandler Type从依赖注入容器中获取AuthenticationHandler ,获取的AuthenticationHandler会被缓存,这样同一个请求的后续调用直接从缓存中拿。
AuthenticationService就是通过它来得到AuthenticationHandler然后完成身份验证各种功能的
public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider
{
public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes)
{
Schemes = schemes;
}
public IAuthenticationSchemeProvider Schemes { get; }
// handler缓存
private readonly Dictionary<string, IAuthenticationHandler> _handlerMap = new Dictionary<string, IAuthenticationHandler>(StringComparer.Ordinal);
public async Task<IAuthenticationHandler?> GetHandlerAsync(HttpContext context, string authenticationScheme)
{
if (_handlerMap.TryGetValue(authenticationScheme, out var value))
{
return value;
}
var scheme = await Schemes.GetSchemeAsync(authenticationScheme);
if (scheme == null)
{
return null;
}
//ActivatorUtilities定义在Microsoft.Extensions.DependencyInjection.Abstractions中
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;
}
}
身份验证服务AuthenticationService
身份验证中的步骤是在多个地方被调用的,身份验证中间件、授权中间件、登录的Action(如:AccountController.SignIn())、注销的Action(如:AccountController.SignOut()),身份验证的核心方法定义在这个类中,但它本质上还是去找到对应的身份验证处理器并调用其同名方法。其实这些方法还进一步以扩展方法的形式定义到HttpContext上了。以SignIn方法为例
HttpContext.SignIn() > AuthenticationService.SignIn() > AuthenticationHandler.SignIn()
IAuthenticationService
IAuthenticationService是用来对外提供一个统一的认证服务接口
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);
}
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;
}
}
AuthenticationResult
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) };
}
AuthenticationHttpContextExtensions
AuthenticationHttpContextExtensions 类是对 HttpContext 认证相关的扩展,在Core2.0中是使用HttpContext的Authentication属性做认证
public static class AuthenticationHttpContextExtensions
{
//验证在 SignInAsync 中颁发的证书,并返回一个 AuthenticateResult 对象,表示用户的身份。
public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) =>
context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, scheme);
//返回一个需要认证的标识来提示用户登录,通常会返回一个 401 状态码
public static Task ChallengeAsync(this HttpContext context, string scheme, AuthenticationProperties properties) { }
//禁止访问,表示用户权限不足,通常会返回一个 403 状态码
public static Task ForbidAsync(this HttpContext context, string scheme, AuthenticationProperties properties) { }
//用户登录成功后颁发一个证书(加密的用户凭证),用来标识用户的身份
public static Task SignInAsync(this HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) {}
//退出登录,如清除Coookie等
public static Task SignOutAsync(this HttpContext context, string scheme, AuthenticationProperties properties) { }
//用来获取 AuthenticationProperties 中保存的额外信息
public static Task<string> GetTokenAsync(this HttpContext context, string scheme, string tokenName) { }
}
以上方法都是通过从DI系统中获取到 IAuthenticationService 接口实例,然后调用其同名方法
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;
}