理解 ASP.NET Core: 认证
ASP.NET Core 认证
通常在应用程序中,安全分为前后两个步骤:验证和授权。验证负责检查当前请求者的身份,而授权则根据上一步得到的身份决定当前请求者是否能够访问期望的资源。
既然安全从验证开始,我们也就从验证开始介绍安全。
验证的核心概念
我们先从比较简单的场景开始考虑,例如在 Web API 开发中,需要验证请求方是否提供了安全令牌,安全令牌是否有效。如果无效,那么 API 端应该拒绝提供服务。在命名空间 Microsoft.AspNetCore.Authentication 下,定义关于验证的核心接口。对应的程序集是 Microsoft.AspNetCore.Authentication.Abstractions.dll。
验证接口 IAuthenticationHandler
在 ASP.NET 下,验证中包含 3 个基本操作:
Authenticate 验证
验证操作负责基于当前请求的上下文,使用来自请求中的信息,例如请求头、Cookie 等等来构造用户标识。构建的结果是一个 AuthenticateResult 对象,它指示了验证是否成功,如果成功的话,用户标识将可以在验证票据中找到。
常见的验证包括:
- 基于 Cookie 的验证,从请求的 Cookie 中验证用户
- 基于 JWT Bearer 的验证,从请求头中提取 JWT 令牌进行验证
Challenge 质询
在授权管理阶段,如果用户没有得到验证,但所期望访问的资源要求必须得到验证的时候,授权服务会发出质询。例如,当匿名用户访问受限资源的时候,或者当用户点击登录链接的时候。授权服务会通过质询来相应用户。
例如
- 基于 Cookie 的验证会将用户重定向到登录页面
- 基于 JWT 的验证会返回一个带有 www-authenticate: bearer 响应头的 401 响应来提醒客户端需要提供访问凭据
质询操作应该让用户知道应该使用何种验证机制来访问请求的资源。
Forbid 拒绝
在授权管理阶段,如果用户已经通过了验证,但是对于其访问的资源并没有得到许可,此时会使用拒绝操作。
例如:
- Cookie 验证模式下,已经登录但是没有访问权限的用户,被重定向到一个提示无权访问的页面
- JWT 验证模式下,返回 403
- 在自定义验证模式下,将没有权限的用户重定向到申请资源的页面
拒绝访问处理应该让用户知道:
- 它已经通过了验证
- 但是没有权限访问请求的资源
在这个场景下,可以看到,验证需要提供的基本功能就包括了验证和验证失败后的拒绝服务两个操作。在 ASP.NET Core 中,验证被称为 Authenticate,拒绝被称为 Forbid。 在供消费者访问的网站上,如果我们希望在验证失败后,不是像 API 一样直接返回一个错误页面,而是将用户导航到登录页面,那么,就还需要增加一个操作,这个操作的本质是希望用户再次提供安全凭据,在 ASP.NET Core 中,这个操作被称为 Challenge。这 3 个操作结合在一起,就是验证最基本的要求,以接口形式表示,就是 IAuthenticationHandler 接口,如下所示:
public interface IAuthenticationHandler
{
Task InitializeAsync(AuthenticationScheme scheme, HttpContext context);
Task<AuthenticateResult> AuthenticateAsync();
Task ChallengeAsync(AuthenticationProperties? properties);
Task ForbidAsync(AuthenticationProperties? properties);
}
验证的结果是一个 AuthenticateResult 对象。值得注意的是,它还提供了一个静态方法 NoResult() 用来返回没有得到结果,静态方法 Fail() 生成一个表示验证异常的结果,而 Success() 成功则需要提供验证票据。
通过验证之后,会返回一个包含了请求者票据的验证结果。
namespace Microsoft.AspNetCore.Authentication
{
public class AuthenticateResult
{
// ......
public static AuthenticateResult NoResult()
{
return new AuthenticateResult() { None = true };
}
public static AuthenticateResult Fail(Exception failure)
{
return new AuthenticateResult() { Failure = failure };
}
public static AuthenticateResult Success(AuthenticationTicket ticket)
{
if (ticket == null)
{
throw new ArgumentNullException(nameof(ticket));
}
return new AuthenticateResult() { Ticket = ticket, Properties = ticket.Properties };
}
public static AuthenticateResult Success(AuthenticationTicket ticket)
{
if (ticket == null)
{
throw new ArgumentNullException(nameof(ticket));
}
return new AuthenticateResult() { Ticket = ticket, Properties = ticket.Properties };
}
// ......
}
}
在 GitHub 中查看 AuthenticateResult 源码
那么验证的信息来自哪里呢?除了前面介绍的 3 个操作之外,还要求一个初始化的操作 Initialize,通过这个方法来提供当前请求的上下文信息。
在 GitHub 中查看 IAuthenticationHandler 定义
支持登录和登出操作的验证接口
有的时候,我们还希望提供登出操作,增加登出操作的接口被称为 IAuthenticationSignOutHandler。
public interface IAuthenticationSignOutHandler : IAuthenticationHandler
{
Task SignOutAsync(AuthenticationProperties? properties);
}
在 GitHub 中查看 IAuthenticationSignOutHandler 源码
在登出的基础上,如果还希望提供登录操作,那么就是 IAuthenticationSignInHandler 接口。
public interface IAuthenticationSignInHandler : IAuthenticationSignOutHandler
{
Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties);
}
在 GitHub 中查看 IAuthenticationSignInHandler 源码
实现验证支持的抽象基类 AuthenticationHandler
直接实现接口还是比较麻烦的,在命名空间 Microsoft.AspNetCore.Authentication 下,微软提供了抽象基类 AuthenticationHandler 以方便验证控制器的开发,其它控制器可以从该控制器派生,以取得其提供的服务。
namespace Microsoft.AspNetCore.Authentication
{
public abstract class AuthenticationHandler<TOptions> : IAuthenticationHandler where TOptions : AuthenticationSchemeOptions, new()
{
protected AuthenticationHandler(IOptionsMonitor<TOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
{
Logger = logger.CreateLogger(this.GetType().FullName);
UrlEncoder = encoder;
Clock = clock;
OptionsMonitor = options;
}
}
// ......
}
通过类的定义可以看到,它使用了泛型。每个控制器应该有一个对应该控制器的配置选项,通过泛型来指定验证处理器所使用的配置类型,在构造函数中,可以看到它被用于获取对应的配置选项对象。
在 GitHub 中查看 AuthenticationHandler 源码
通过 InitializeAsync(),验证处理器可以获得当前请求的上下文对象 HttpContext。
public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
最终,作为抽象类的 ,希望派生类来完成这个验证任务,抽象方法 HandleAuthenticateAsync() 提供了扩展点。
/// <summary>
/// Allows derived types to handle authentication.
/// </summary>
/// <returns>The <see cref="AuthenticateResult"/>.</returns>
protected abstract Task<AuthenticateResult> HandleAuthenticateAsync();
验证的结果是一个 AuthenticateResult。
而拒绝服务则简单的多,直接在这个抽象基类中提供了默认实现。直接返回 HTTP 403。
protected virtual Task HandleForbiddenAsync(AuthenticationProperties properties)
{
Response.StatusCode = 403;
return Task.CompletedTask;
}
剩下的一个也一样,提供了默认实现。直接返回 HTTP 401 响应。
protected virtual Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.StatusCode = 401;
return Task.CompletedTask;
}
Jwt 验证处理器是如何实现的?
对于 JWT 来说,并不涉及到登入和登出,所以它需要从实现 IAuthenticationHandler 接口的抽象基类 AuthenticationHandler 派生出来即可。从 AuthenticationHandler 派生出来的 JwtBearerHandler 实现基于自己的配置选项 JwtBearerOptions。所以该类定义就变得如下所示,而构造函数显然配合了抽象基类的要求。
namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
public class JwtBearerHandler : AuthenticationHandler<JwtBearerOptions>
{
public JwtBearerHandler(
IOptionsMonitor<JwtBearerOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{ }
// ......
}
}
在 GitHub 中查看 JwtBearerHandler 源码
真正的验证则在 HandleAuthenticateAsync() 中实现。下面的代码是不是就很熟悉了,从请求头中获取附带的 JWT 访问令牌,然后验证该令牌的有效性,核心代码如下所示。
string authorization = Request.Headers[HeaderNames.Authorization];
// If no authorization header found, nothing to process further
if (string.IsNullOrEmpty(authorization))
{
return AuthenticateResult.NoResult();
}
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
token = authorization.Substring("Bearer ".Length).Trim();
}
// If no token found, no further work possible
if (string.IsNullOrEmpty(token))
{
return AuthenticateResult.NoResult();
}
// ......
principal = validator.ValidateToken(token, validationParameters, out validatedToken);
在 GitHub 中查看 JwtBearerHandler 源码
注册 Jwt 验证处理器
在 ASP.NET Core 中,你可以使用各种验证处理器,并不仅仅只能使用一个,验证控制器需要一个名称,它被看作该验证模式 Schema 的名称。Jwt 验证模式的默认名称就是 "Bearer",通过字符串常量 JwtBearerDefaults.AuthenticationScheme 定义。
namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
/// <summary>
/// Default values used by bearer authentication.
/// </summary>
public static class JwtBearerDefaults
{
/// <summary>
/// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions
/// </summary>
public const string AuthenticationScheme = "Bearer";
}
}
在 GitHub 中查看 JwtBearerDefaults 源码
最终通过 AuthenticationBuilder 的扩展方法 AddJwtBearer() 将 Jwt 验证控制器注册到依赖注入的容器中。
public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder)
=> builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, _ => { });
public static AuthenticationBuilder AddJwtBearer(
this AuthenticationBuilder builder,
string authenticationScheme,
string displayName,
Action<JwtBearerOptions> configureOptions)
{
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>,
JwtBearerPostConfigureOptions>());
return builder.AddScheme<JwtBearerOptions, JwtBearerHandler>(
authenticationScheme, displayName, configureOptions);
}
在 GitHub 中查看 JwtBearerExtensions 扩展方法源码
验证架构 Schema
一种验证处理器,加上对应的验证配置选项,我们再为它起一个名字,组合起来就成为一种验证架构 Schema。在 ASP.NET Core 中,可以注册多种验证架构。例如,授权策略可以使用架构的名称来指定所使用的验证架构来使用特定的验证方式。在配置验证的时候,通常设置默认的验证架构。当没有指定验证架构的时候,就会使用默认架构进行处理。
还可以
- 对于 authenticate, challenge, 以及 forbid 操作使用不同的验证架构
- 使用策略来组合多种验证架构
注册的验证模式,最终变成 AuthenticationScheme,注册到依赖注入服务中。
public class AuthenticationScheme
{
public string Name { get; }
public string? DisplayName { get; }
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
public Type HandlerType { get; }
}
在 GitHub 中查看 AuthenticationScheme 源码
使用验证处理器
IAuthenticationSchemeProvider
各种验证架构被保存到一个 IAuthenticationSchemeProvider 中。
public interface IAuthenticationSchemeProvider
{
Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync();
Task<AuthenticationScheme?> GetSchemeAsync(string name);
void AddScheme(AuthenticationScheme scheme);
void RemoveScheme(string name);
}
在 GitHub 中查看 IAuthenticationSchemeProvider 源码
IAuthenticationHandlerProvider
最终的使用是通过 IAuthenticationHandlerProvider 来实现的,通过一个验证模式的字符串名称,可以取得所对应的验证控制器。
public interface IAuthenticationHandlerProvider
{
Task<IAuthenticationHandler?> GetHandlerAsync(HttpContext context, string authenticationScheme);
}
在 GitHub 中查看 IAuthenticationHandlerProvider 源码
它的默认实现是 AuthenticationHandlerProvider,源码并不复杂。
public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider
{
public IAuthenticationSchemeProvider Schemes { get; }
private readonly Dictionary<string, IAuthenticationHandler> _handlerMap
= new Dictionary<string, IAuthenticationHandler>(StringComparer.Ordinal);
public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes)
{
Schemes = schemes;
}
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;
}
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;
}
}
在 GitHub 中查看 AuthenticationHandlerProvider 源码
Authentication 中间件 AuthenticationMiddleware
验证中间件的处理就没有那么复杂了。
找到默认的验证模式,使用默认验证模式的名称取得对应的验证处理器,如果验证成功的话,把当前请求用户的主体放到当前请求上下文的 User 上。
里面还有一段特别的代码,用来找出哪些验证处理器实现了 IAuthenticationHandlerProvider,并依次调用它们,看看是否需要提取终止请求处理过程。
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Authentication
{
public class AuthenticationMiddleware
{
private readonly RequestDelegate _next;
public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
if (schemes == null)
{
throw new ArgumentNullException(nameof(schemes));
}
_next = next;
Schemes = schemes;
}
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
});
// Give any IAuthenticationRequestHandler schemes a chance to handle the request
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);
}
}
}
在 GitHub 中查看 AuthenticationMiddle 源码