通常在应用程序中,安全分为前后两个步骤:验证和授权。验证负责检查当前请求者的身份,而授权则根据上一步得到的身份决定当前请求者是否能够访问期望的资源。 既然安全从验证开始,我们也就从验证开始介绍安全。验证的核心概念 我们先从比较简单的场景开始考虑,例如在WebAPI开发中,需要验证请求方是否提供了安全令牌,安全令牌是否有效。如果无效,那么API端应该拒绝提供服务。在命名空间Microsoft。AspNetCore。Authentication下,定义关于验证的核心接口。对应的程序集是Microsoft。AspNetCore。Authentication。Abstractions。dll。验证接口IAuthenticationHandler 在ASP。NET下,验证中包含3个基本操作:Authenticate验证 验证操作负责基于当前请求的上下文,使用来自请求中的信息,例如请求头、Cookie等等来构造用户标识。构建的结果是一个AuthenticateResult对象,它指示了验证是否成功,如果成功的话,用户标识将可以在验证票据中找到。 常见的验证包括:基于Cookie的验证,从请求的Cookie中验证用户基于JWTBearer的验证,从请求头中提取JWT令牌进行验证Challenge质询 在授权管理阶段,如果用户没有得到验证,但所期望访问的资源要求必须得到验证的时候,授权服务会发出质询。例如,当匿名用户访问受限资源的时候,或者当用户点击登录链接的时候。授权服务会通过质询来相应用户。 例如基于Cookie的验证会将用户重定向到登录页面基于JWT的验证会返回一个带有wwwauthenticate:bearer响应头的401响应来提醒客户端需要提供访问凭据 质询操作应该让用户知道应该使用何种验证机制来访问请求的资源。Forbid拒绝 在授权管理阶段,如果用户已经通过了验证,但是对于其访问的资源并没有得到许可,此时会使用拒绝操作。 例如:Cookie验证模式下,已经登录但是没有访问权限的用户,被重定向到一个提示无权访问的页面JWT验证模式下,返回403在自定义验证模式下,将没有权限的用户重定向到申请资源的页面 拒绝访问处理应该让用户知道:它已经通过了验证但是没有权限访问请求的资源 在这个场景下,可以看到,验证需要提供的基本功能就包括了验证和验证失败后的拒绝服务两个操作。在ASP。NETCore中,验证被称为Authenticate,拒绝被称为Forbid。在供消费者访问的网站上,如果我们希望在验证失败后,不是像API一样直接返回一个错误页面,而是将用户导航到登录页面,那么,就还需要增加一个操作,这个操作的本质是希望用户再次提供安全凭据,在ASP。NETCore中,这个操作被称为Challenge。这3个操作结合在一起,就是验证最基本的要求,以接口形式表示,就是IAuthenticationHandler接口,如下所示:publicinterfaceIAuthenticationHandler{TaskInitializeAsync(AuthenticationSchemescheme,HttpContextcontext);TaskAuthenticateAsync();TaskChallengeAsync(AuthenticationProperties?properties);TaskForbidAsync(AuthenticationProperties?properties);} 验证的结果是一个AuthenticateResult对象。值得注意的是,它还提供了一个静态方法NoResult()用来返回没有得到结果,静态方法Fail()生成一个表示验证异常的结果,而Success()成功则需要提供验证票据。 通过验证之后,会返回一个包含了请求者票据的验证结果。namespaceMicrosoft。AspNetCore。Authentication{publicclassAuthenticateResult{。。。。。。publicstaticAuthenticateResultNoResult(){returnnewAuthenticateResult(){Nonetrue};}publicstaticAuthenticateResultFail(Exceptionfailure){returnnewAuthenticateResult(){Failurefailure};}publicstaticAuthenticateResultSuccess(AuthenticationTicketticket){if(ticketnull){thrownewArgumentNullException(nameof(ticket));}returnnewAuthenticateResult(){Ticketticket,Propertiesticket。Properties};}publicstaticAuthenticateResultSuccess(AuthenticationTicketticket){if(ticketnull){thrownewArgumentNullException(nameof(ticket));}returnnewAuthenticateResult(){Ticketticket,Propertiesticket。Properties};}。。。。。。}} 在GitHub中查看AuthenticateResult源码 那么验证的信息来自哪里呢?除了前面介绍的3个操作之外,还要求一个初始化的操作Initialize,通过这个方法来提供当前请求的上下文信息。 在GitHub中查看IAuthenticationHandler定义支持登录和登出操作的验证接口 有的时候,我们还希望提供登出操作,增加登出操作的接口被称为IAuthenticationSignOutHandler。publicinterfaceIAuthenticationSignOutHandler:IAuthenticationHandler{TaskSignOutAsync(AuthenticationProperties?properties);} 在GitHub中查看IAuthenticationSignOutHandler源码 在登出的基础上,如果还希望提供登录操作,那么就是IAuthenticationSignInHandler接口。publicinterfaceIAuthenticationSignInHandler:IAuthenticationSignOutHandler{TaskSignInAsync(ClaimsPrincipaluser,AuthenticationProperties?properties);} 在GitHub中查看IAuthenticationSignInHandler源码实现验证支持的抽象基类AuthenticationHandler 直接实现接口还是比较麻烦的,在命名空间Microsoft。AspNetCore。Authentication下,微软提供了抽象基类AuthenticationHandler以方便验证控制器的开发,其它控制器可以从该控制器派生,以取得其提供的服务。namespaceMicrosoft。AspNetCore。Authentication{publicabstractclassAuthenticationHandlerTOptions:IAuthenticationHandlerwhereTOptions:AuthenticationSchemeOptions,new(){protectedAuthenticationHandler(IOptionsMonitorTOptionsoptions,ILoggerFactorylogger,UrlEncoderencoder,ISystemClockclock){Loggerlogger。CreateLogger(this。GetType()。FullName);UrlEncoderencoder;Clockclock;OptionsMonitoroptions;}}。。。。。。} 通过类的定义可以看到,它使用了泛型。每个控制器应该有一个对应该控制器的配置选项,通过泛型来指定验证处理器所使用的配置类型,在构造函数中,可以看到它被用于获取对应的配置选项对象。 在GitHub中查看AuthenticationHandler源码 通过InitializeAsync(),验证处理器可以获得当前请求的上下文对象HttpContext。publicasyncTaskInitializeAsync(AuthenticationSchemescheme,HttpContextcontext) 最终,作为抽象类的,希望派生类来完成这个验证任务,抽象方法HandleAuthenticateAsync()提供了扩展点。summaryAllowsderivedtypestohandleauthentication。summaryreturnsTheseecrefAuthenticateResult。returnsprotectedabstractTaskHandleAuthenticateAsync(); 验证的结果是一个AuthenticateResult。 而拒绝服务则简单的多,直接在这个抽象基类中提供了默认实现。直接返回HTTP403。protectedvirtualTaskHandleForbiddenAsync(AuthenticationPropertiesproperties){Response。StatusCode403;returnTask。CompletedTask;} 剩下的一个也一样,提供了默认实现。直接返回HTTP401响应。protectedvirtualTaskHandleChallengeAsync(AuthenticationPropertiesproperties){Response。StatusCode401;returnTask。CompletedTask;}Jwt验证处理器是如何实现的? 对于JWT来说,并不涉及到登入和登出,所以它需要从实现IAuthenticationHandler接口的抽象基类AuthenticationHandler派生出来即可。从AuthenticationHandler派生出来的JwtBearerHandler实现基于自己的配置选项JwtBearerOptions。所以该类定义就变得如下所示,而构造函数显然配合了抽象基类的要求。namespaceMicrosoft。AspNetCore。Authentication。JwtBearer{publicclassJwtBearerHandler:AuthenticationHandlerJwtBearerOptions{publicJwtBearerHandler(IOptionsMonitorJwtBearerOptionsoptions,ILoggerFactorylogger,UrlEncoderencoder,ISystemClockclock):base(options,logger,encoder,clock){}。。。。。。}} 在GitHub中查看JwtBearerHandler源码 真正的验证则在HandleAuthenticateAsync()中实现。下面的代码是不是就很熟悉了,从请求头中获取附带的JWT访问令牌,然后验证该令牌的有效性,核心代码如下所示。stringauthorizationRequest。Headers〔HeaderNames。Authorization〕;Ifnoauthorizationheaderfound,nothingtoprocessfurtherif(string。IsNullOrEmpty(authorization)){returnAuthenticateResult。NoResult();}if(authorization。StartsWith(Bearer,StringComparison。OrdinalIgnoreCase)){tokenauthorization。Substring(Bearer。Length)。Trim();}Ifnotokenfound,nofurtherworkpossibleif(string。IsNullOrEmpty(token)){returnAuthenticateResult。NoResult();}。。。。。。principalvalidator。ValidateToken(token,validationParameters,outvalidatedToken); 在GitHub中查看JwtBearerHandler源码注册Jwt验证处理器 在ASP。NETCore中,你可以使用各种验证处理器,并不仅仅只能使用一个,验证控制器需要一个名称,它被看作该验证模式Schema的名称。Jwt验证模式的默认名称就是Bearer,通过字符串常量JwtBearerDefaults。AuthenticationScheme定义。namespaceMicrosoft。AspNetCore。Authentication。JwtBearer{summaryDefaultvaluesusedbybearerauthentication。summarypublicstaticclassJwtBearerDefaults{summaryDefaultvalueforAuthenticationSchemepropertyintheJwtBearerAuthenticationOptionssummarypublicconststringAuthenticationSchemeBearer;}} 在GitHub中查看JwtBearerDefaults源码 最终通过AuthenticationBuilder的扩展方法AddJwtBearer()将Jwt验证控制器注册到依赖注入的容器中。publicstaticAuthenticationBuilderAddJwtBearer(thisAuthenticationBuilderbuilder)builder。AddJwtBearer(JwtBearerDefaults。AuthenticationScheme,{});publicstaticAuthenticationBuilderAddJwtBearer(thisAuthenticationBuilderbuilder,stringauthenticationScheme,stringdisplayName,ActionJwtBearerOptionsconfigureOptions){builder。Services。TryAddEnumerable(ServiceDescriptor。SingletonIPostConfigureOptionsJwtBearerOptions,JwtBearerPostConfigureOptions());returnbuilder。AddSchemeJwtBearerOptions,JwtBearerHandler(authenticationScheme,displayName,configureOptions);} 在GitHub中查看JwtBearerExtensions扩展方法源码验证架构Schema 一种验证处理器,加上对应的验证配置选项,我们再为它起一个名字,组合起来就成为一种验证架构Schema。在ASP。NETCore中,可以注册多种验证架构。例如,授权策略可以使用架构的名称来指定所使用的验证架构来使用特定的验证方式。在配置验证的时候,通常设置默认的验证架构。当没有指定验证架构的时候,就会使用默认架构进行处理。 还可以对于authenticate,challenge,以及forbid操作使用不同的验证架构使用策略来组合多种验证架构 注册的验证模式,最终变成AuthenticationScheme,注册到依赖注入服务中。publicclassAuthenticationScheme{publicstringName{get;}publicstring?DisplayName{get;}〔DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes。PublicConstructors)〕publicTypeHandlerType{get;}} 在GitHub中查看AuthenticationScheme源码使用验证处理器IAuthenticationSchemeProvider 各种验证架构被保存到一个IAuthenticationSchemeProvider中。publicinterfaceIAuthenticationSchemeProvider{TaskIEnumerableGetAllSchemesAsync();TaskGetSchemeAsync(stringname);voidAddScheme(AuthenticationSchemescheme);voidRemoveScheme(stringname);} 在GitHub中查看IAuthenticationSchemeProvider源码IAuthenticationHandlerProvider 最终的使用是通过IAuthenticationHandlerProvider来实现的,通过一个验证模式的字符串名称,可以取得所对应的验证控制器。publicinterfaceIAuthenticationHandlerProvider{TaskIAuthenticationHandler?GetHandlerAsync(HttpContextcontext,stringauthenticationScheme);} 在GitHub中查看IAuthenticationHandlerProvider源码 它的默认实现是AuthenticationHandlerProvider,源码并不复杂。publicclassAuthenticationHandlerProvider:IAuthenticationHandlerProvider{publicIAuthenticationSchemeProviderSchemes{get;}privatereadonlyDictionarystring,IAuthenticationHandlerhandlerMapnewDictionarystring,IAuthenticationHandler(StringComparer。Ordinal);publicAuthenticationHandlerProvider(IAuthenticationSchemeProviderschemes){Schemesschemes;}publicasyncTaskIAuthenticationHandler?GetHandlerAsync(HttpContextcontext,stringauthenticationScheme){if(handlerMap。TryGetValue(authenticationScheme,outvarvalue)){returnvalue;}varschemeawaitSchemes。GetSchemeAsync(authenticationScheme);if(schemenull){returnnull;}varhandler(context。RequestServices。GetService(scheme。HandlerType)??ActivatorUtilities。CreateInstance(context。RequestServices,scheme。HandlerType))asIAuthenticationHandler;if(handler!null){awaithandler。InitializeAsync(scheme,context);handlerMap〔authenticationScheme〕handler;}returnhandler;}} 在GitHub中查看AuthenticationHandlerProvider源码Authentication中间件AuthenticationMiddleware 验证中间件的处理就没有那么复杂了。 找到默认的验证模式,使用默认验证模式的名称取得对应的验证处理器,如果验证成功的话,把当前请求用户的主体放到当前请求上下文的User上。 里面还有一段特别的代码,用来找出哪些验证处理器实现了IAuthenticationHandlerProvider,并依次调用它们,看看是否需要提取终止请求处理过程。usingSystem;usingSystem。Threading。Tasks;usingMicrosoft。AspNetCore。Http;usingMicrosoft。Extensions。DependencyInjection;namespaceMicrosoft。AspNetCore。Authentication{publicclassAuthenticationMiddleware{privatereadonlyRequestDelegatenext;publicAuthenticationMiddleware(RequestDelegatenext,IAuthenticationSchemeProviderschemes){if(nextnull){thrownewArgumentNullException(nameof(next));}if(schemesnull){thrownewArgumentNullException(nameof(schemes));}nextnext;Schemesschemes;}publicIAuthenticationSchemeProviderSchemes{get;set;}publicasyncTaskInvoke(HttpContextcontext){context。Features。SetIAuthenticationFeature(newAuthenticationFeature{OriginalPathcontext。Request。Path,OriginalPathBasecontext。Request。PathBase});GiveanyIAuthenticationRequestHandlerschemesachancetohandletherequestvarhandlerscontext。RequestServices。GetRequiredServiceIAuthenticationHandlerProvider();foreach(varschemeinawaitSchemes。GetRequestHandlerSchemesAsync()){varhandlerawaithandlers。GetHandlerAsync(context,scheme。Name)asIAuthenticationRequestHandler;if(handler!nullawaithandler。HandleRequestAsync()){return;}}vardefaultAuthenticateawaitSchemes。GetDefaultAuthenticateSchemeAsync();if(defaultAuthenticate!null){varresultawaitcontext。AuthenticateAsync(defaultAuthenticate。Name);if(result?。Principal!null){context。Userresult。Principal;}}awaitnext(context);}}} 在GitHub中查看AuthenticationMiddle源码参考资料https:docs。microsoft。comenusaspnetcoresecurityauthentication?viewaspnetcore5。0