ASP.NET没有魔法——ASP.NET Identity 的“多重”身份验证
ASP.NET Identity除了提供基于Cookie的身份验证外,还提供了一些高级功能,如多次输入错误账户信息后会锁定用户禁止登录、集成第三方验证、账户的二次验证等,并且ASP.NET MVC的默认模板中就带有这些功能。
本文将从以下几个方面解释ASP.NET Identity是如何实现身份验证机制的:
● ASP.NET Identity的“多重”身份验证
● Owin身份验证的积极模式与消极模式
● 再谈Owin身份验证机制
● 基于Owin的Identity在ASP.NET中身份验证的解决方案
ASP.NET Identity的“多重”身份验证
为什么本章以Identity的多重身份验证为题?因为ASP.NET Identity是基于Owin通过中间件的形式实现的身份验证,以下是默认模板中添加的代码:
从代码中可以看到,模板代码一共添加了7个(包含被注释的3个)身份验证相关的中间件,按照对Owin中间件的理解,当一个请求进入Owin管道后,每一个中间件都会被执行,上面的身份验证中间件按照功能来分有基于Cookie的、有基于外部Cookie的、双因子验证的以及第三方账户登录(被注释的代码)的,这就意味着每一个请求都会被这些中间件一一处理,所以称其为“多重”身份验证。
本文从标题到现在的“多重”均是打引号的,说明Identity中的验证并不像表面那样多个功能堆叠那么简单,接下来就从身份验证的积极模式与消极模式开始来消除Identity在ASP.NET身份验证中的迷雾。
Owin身份验证的积极模式与消极模式
Identity是基于Owin实现身份验证的,所以实际上Owin才是身份验证规则的制定者,Identity只不过是实现的一种,在Owin中把身份验证中间件分为两个模式,分别是积极模式与消极模式,两个模式是通过一个名为AuthenticationMode的枚举类型定义的:
根据代码注释得到对积极模式以及消极模式的介绍如下:
● 积极模式(Active):积极模式的身份验证中间件将在请求到达时对用户身份信息进行修改,同时当响应状态为401时会处理响应信息。
● 消极模式(Passive):该模式下的中间件只有需要或者说显式调用的情况下才会使用该中间件包含的方法对请求进行验证,包括当出现401情况时也需要在拓展的Challenge数据中找到对应的验证类型名称匹配后才会调用处理。
一句话来说就是无论在Owin管道中添加多少个身份验证中间件,在处理请求时都只有验证模式为积极的才会对请求进行处理,其它的都需要通过手动的方式来调用。为什么?看后面内容。
再谈Owin身份验证机制
前面的文章中介绍了Owin与Identity是如何集成的如《ASP.NET没有魔法——Identity与Owin》、《ASP.NET没有魔法——ASP.NET Identity的加密与解密》等文章,但主要是介绍Identity如何通过中间件的方式添加到Owin管道对用户身份信息验证以及基于Cookie的身份验证中间件做了什么工作,最核心的点还未提到,那就是整个身份验证的机制或者说规则是由Owin(Katana)来定的,Identity只不过是遵循这个规则的一个实现甚至可以说只是一个使用者,因为它仅仅是为Cookie验证中间件提供了用户数据,然后通过依赖AuthenticationManager来实现、拓展了ASP.NET中的身份验证功能。
上面是Owin身份验证相关的包图,从这个图中可以得出以下结论:
1. Micrsosft.Owin中定义了Owin相关的主体如上下文、管道Builder以及身份验证相关的业务逻辑AuthenticationManager。
2. Microsoft.Owin.Security作为Owin安全相关功能的补充,定义了身份验证的模式(积极和消极)以及用于身份验证的中间件和处理器基类。
3. Microsoft.Owin.Security.XXX类型作为真实的身份验证逻辑实现者提供了各种不同的基于Cookie的、Token的甚至第三方账户等的身份验证逻辑。
4. Microsoft.AspNet.Identity.Owin有两个主要对象,SignInManager封装了登录时的业务逻辑,这个业务逻辑既包括了用户数据的操作(UserManager)和身份验证的逻辑,提供了普通登录、双因子登录、外部账户登录等高级功能,而AuthenticationManagerExtensions同样也是针对这些高级功能对AuthenticationManager的拓展。
AuthenticationManager
AuthenticationManager作为Owin中身份验证的业务的核心,其接口定义如下:
根据注释看出它用于与串联到管道的身份验证中间件进行交互,并且提供了身份验证AuthenticateAsync、拒绝Challenge(我将其翻译拒绝或质疑等意思,换句话就是身份验证未通过)、登入SignIn、登出SignOut以及获取验证方式GetAuthenticationTypes等方法。
对于AuthenticationManager可以这么理解,每一次请求都会创建一个AuthenticationManager,它携带了当前请求的用户信息User,然后管理所有串联到Owin管道的身份验证中间件,通过这一系列的中间件完成了用户的验证与拒绝、登入与登出等功能。
需要注意的是AuthenticateAsync返回的是一个验证结果,该结果中包含除了验证结果外还包含了用户信息,而SignIn方法实际上是将用户信息添加到(或者说替换)当前请求上下文的用户信息,所以大部分情况下AuthenticateAsync和SignIn方法是连续使用的。
AuthenticationMiddleware&AuthenticationHandler
AuthenticationMiddleware和AuthenticationHandler作为模板代码,对中间件的执行以及处理器的处理方法进行了限制:
AuthenticationMiddleware的执行方法:
分为四个阶段:1. 处理器的创建。2. 处理器的初始化。3. 处理器的调用。4. 处理器的销毁。
对于AuthenticationHandler来说,它有几个重要的过程:
1. 初始化:将当前处理器注册到AuthenticationManager中,另外如果当前中间件的模式为积极模式,那么调用身份验证方法,否则身份验证方法就只能通过AuthenticationManager调用。(注:InitializeCoreAsync的具体实现在子类中)
2. 执行:
从上面的分析可以知道,在身份验证处理其中有一个专门用于身份验证的方法AuthenticateAsync,那么执行方法是做什么用的呢?在基于Identity的ASP.NET应用程序中身份验证方法只有在积极模式下时才会对自动对请求进行身份验证,并且无论是否通过验证都不会对请求进行任何的处理(即使没有通过身份验证仍旧能够将请求送到Controller、Action的执行,因为它们是可以被匿名调用的),而有一种情况就是当服务器处理请求时就能够确定该请求应该如何处理,如通过第三方账户登录后会在Url的查询字符串上携带一些Token等信息,那么这个时候能够确定的就是需要根据这些信息进行处理进而完成身份验证而不是还可以将请求交给Controler处理,在这种情况下就可以将这些处理的逻辑放到Invoke方法中,Invoke方法如果返回true时,管道将不再继续往下执行(见中间件的Invoke方法)。微软、Google等社交账户验证时,它的验证逻辑是写在这个InvokeAsync方法中的,而Cookie验证的中间件就默认返回false将请求交与后续组件处理,后续详细介绍Owin的第三方验证。
3. 销毁:当请求处于返回阶段时,会触发身份验证处理器的销毁过程,整个过程包括将身份信息写入响应信息中(如Cookie验证会将AuthenticationTicket对象序列化加密后写到Cookie中),并执行子类的一些销毁逻辑,最后将添加到AuthenticationManager中的处理器删除。
SignInManager&AuthenticationManagerExtensions
SignInManager用于管理用户的登录逻辑,如登录、使用密码登录、双因子登录、外部登录等等,它是对UserManager与AuthenticationManager的封装:
AuthenticationManagerExtensions是对AuthenticationManager的拓展,主要是对AuthenticationManager添加了外部验证以及双因子验证的补充支持,这些拓展方法被SignInManager以及身份验证Controller使用:
基于Owin的Identity在ASP.NET中身份验证的解决方案
上面介绍了Owin以及Identity中主要的组件及其功能,那么在ASP.NET中是如何使用这些功能实现身份验证功能的呢?在ASP.NET中主要把身份验证分为这三种类型:
● 普通验证:普通验证就是之前文章中介绍过的基于Cookie的身份验证方式,其过程是用户提交用户名及密码,服务器完成密码验证后生成用户信息,后续请求根据这个用户信息即可验证用户身份。
● 双因子验证:双因子验证在普通验证基础上增加了如短信验证码、邮件验证码等信息的二次验证,就是说服务器根据用户名密码完成用户验证后,该用户仍然处于“未通过”的身份验证状态,还需要对发送给客户的验证码进行二次校验后才完成身份验证。
● 外部验证:既用户数据在应用程序外部,如各种不同的社交账户,通过这些服务商提供的身份验证接口登陆后,返回一系列的服务访问Token、用户信息等的验证方式。
这里将通过ASP.NET MVC带身份验证功能的模板代码来介绍以上三种方式是如何使用Identity组件实现的。
普通验证
Identity在ASP.NET中通过Cookie验证的方式实现了普通的用户身份验证,其主要过程如下(注:左边为主要流程,右边为每一个主流程包含的子流程):
上图是ASP.NET MVC中通过Identity实现基于Cookie的用户身份验证的用户信息生成过程,它有几个需要注意的点:
1. 首先将用户名、密码等信息通过可匿名访问的AccountControllter中的Login方法提交到服务器,然后通过SignInManager的PasswordSignInAsync实现了用户名、密码的验证以及登录,最后在请求返回阶段将用户身份信息加密后写到Cookie中。
2. 在密码验证/登录阶段除了对用户密码进行验证外,还有一些用户是否被锁定、登录失败次数计数以及重置等判断,用于实现用户的锁定以及多次登录失败自动锁定用户等功能。
3.SignInManager.SignInOrTwoFactor方法根据验证方法的配置决定是双因子验证还是普通验证,在普通验证情况下,通过获取用户身份信息以及新建身份验证属性来完成后续的验证管理器的登入操作(注:上面在介绍身份验证管理器时说过,它的AuthenticateAsync和SignIn方法是连续使用的,前者获取用户信息,后者将用户信息写到请求上下文中,但是因为现在是登录操作,请求中还没有用户身份信息,所以用户身份信息以及验证属性需要自己创造,比如通过数据库获取,这里通过自己创造的用户信息然后调用SignIn方法实现了登录将用户信息写到请求上下文中)。
4.当请求返回时,将写到请求上下文中的用户信息以Cookie的形式携带到客户端,以便于后续请求的身份验证。
上图是当用户通过登录后,访问其它资源时候的身份验证过程,这里整个处理过程都是Cookie验证中间件完成的,需要注意的有以下几点:
1. Cookie验证中间件默认是积极模式,所以在处理器初始化时就会调用AuthenticateAsync方法,对请求中的身份信息Cookie进行解析、验证。
2. 当完成身份信息的验证后,该中间件通过AddUserIdentity这个方法将用户信息写到请求上下文中(该方法实际上是替换了AuthenticationManager的SignIn方法)。
3. 当请求返回阶段,如果状态正常那么继续将身份信息写到Cookie中,但是如果身份验证未通过(如超时等情况),请求就回被处理器的ApplyResponseChallengeAsync处理(如跳转到登录页面等功能)。
双因子验证
双因子验证是在普通验证的基础上引入了第二次验证,也就是说在登录时会有两次请求:
上图就是双因子验证的过程,双因子验证实际上也是基于普通验证的,它仅仅是通过配置的形式开启,在身份验证过程中加入了验证码的发送与校验过程,它需要注意以下几点:
1. 在首次登录时和普通验证一样,需要对用户密码、是否被锁定等信息进行验证,然后因为需要双因子验证,所以创建了一个TwoFactorCookie的身份信息并执行了管理器的SignIn方法,该信息保存了通过密码验证的用户名。完成请求后, app.UseTwoFactorSignInCookie方法添加Cookie验证中间件将会把上面创建的TwoFactorCookie身份信息写到Cookie中(注:在管道中添加了多个cookie的验证中间件,但是它的配置和身份验证类型是不同的,这里是根据用户信息的身份验证方式来选择使用哪一个中间件来处理生成Cookie,而ClaimsIdentity构造传入的值就是身份验证方式)。
2. ASP.NET MVC程序通过重定向的方式,将完成第一次验证的请求转到了验证码发送(选择发送方式)及验证码填写页面,填写完验证码并提交后进入第二次验证。
3. 在第二次验证过程中AccountController的VerifyCode方法通过SignInManager的TwoFactorSignInAsync方法完成了验证,该方法的核心是通过AuthenticationManager显式的调用了“TwoFactorCookie”的验证,该验证是 app.UseTwoFactorSignInCookie方法添加的一个消极模式的身份验证中间件。(注:TwoFactorCookie常量的值就是TwoFactorCookie,AuthenticationManager的SignIn方法通过字符串匹配的形式来查找并调用对应的身份验证方法)
这个验证方法就是获取上一次存储的Cookie并解密后得到用户名,获得对应用户信息后,通过UserTokenProvider对验证码进行校验,当所有信息通过校验后,将使用AuthenticationManager的SignIn方法将用户信息添加到请求上下文中(注:关于验证码的生成和校验在后续章节中介绍)。
4. 在VerifyCode请求响应的时候将用户信息通过积极模式的Cookie中间件将用户信息写到Cookie中,该过程与普通验证一致。
注:后续请求与普通验证一致,使用Cookie携带的身份信息,通过积极模式的Cookie身份验证中间件完成身份验证。
外部验证
外部验证即将用户的身份验证过程转交给外部服务器(如各大社交平台等),相对于上面两种验证的流程来说,外部验证的流程更为复杂,其流程图如下:
(注:由于流程图太大,将其分为两个部分,第一部分为第三方登录页面的跳转,第二部分为第三方登录完成后重定向回来的ASP.NET身份验证过程)
ASP.NET第三方登录页面的调整流程比较简单,在默认的模板项目中整个功能就是由AccountController中的ExternalLogin Action方法发起的,首先需要的就是在页面上选择一种第三方账号登录方式,提交到服务端后,ExternalLogin方法直接通过AuthencationManager的拒绝(Challenge)方法拒绝当前请求,将请求重定向到了第三方的登录页面。
当第三方登录成功后会重定回AccountController的ExternalLoginCallback,整个流程的说明如下:
1. 首先需要提到的是模板项目在身份验证管道中通过app.UseExternalSignInCookie方法设置了默认的登录验证方式(ExternalCookie)以及添加了一个消极模式的Cookie验证中间件,其验证方式也是ExternalCookie。
2. 由于请求中携带了由微软(此处以微软为例)身份验证后的信息,所以会被特定身份验证中间件的Invoke方法处理,其处理过程是先调用了对应的身份验证方法获得身份信息,将验证后的身份信息添加默认的身份验证类型(注:该默认类型就是ExternalCookie,其目的是为了将所有通过第三方身份验证的账户统一处理),最后将身份信息通过SignIn方法写到上下文中,并返回True(注:上面提到过,当身份验证处理器的Invoke方法返回true时后续的内容将不会被调用)。
3. 在从微软身份验证处理中间件的返回过程中,由于上下文中的用户信息的身份验证模式是ExternalCookie,所以会被ExternalCookie这个中间件的ApplyResponseGrantAsync方法处理,将当前的身份信息序列化并加密后保存到名为.AspNet.ExternalCookie的cookie中,然后重定向到AcountController的ExternalLoginCallback。
4. ExternalLoginCallback方法实际才是ASP.NET中真正用来身份验证的步骤,前面仅仅是通过第三方获得验证后的用户名等信息,然后将该用户的信息以cookie的形式保存到ExternalCookie中,而这里首先就是通过AuthenticationManager调用了ExternalCookie中间件的身份验证方法,获得Cookie中保存的用户信息,然后通过这个用户信息去本地数据库中查询,如果用户存在,那么登录成功,否则跳转到ExternalLoginConfirmation页面去补充用户信息(相当于根据第三方用户名完成注册)后登录,登录成功后用户信息会被积极模式的Cookie身份验证中间件保存到.AspNet.ApplicationCookie中。(注:这里的登录过程可参考普通验证的登录过程)。
以上就是三种身份验证的执行流程,虽然它们流程不同,但最后目的是一致的,在每一种身份验证流程最后都是通过SignInManager的SignIn方法完成的登录,即无论什么方式登录的,最终都会将身份信息以Cookie的形式保存在名为.AspNet.ApplicationCookie的Cookie中。并且该方法会将双因子验证以及外部Cookie验证的信息都清除:
另外双因子验证作为一种附加的验证方式,它既可以附加到普通验证方式上也可以附加到第三方验证方式上。
小结
本章是对Owin的身份验证机制以及ASP.NET MVC基于Identity的身份验证解决方案进行了介绍,ASP.NET MVC使用Identity基于Owin提供的三种验证方式已经能够满足日常的开发需求,对这些流程的理解能够更好的根据需求来完善自己项目中的身份验证功能,在下一篇文章中将以代码的形式在My Blog中添加双因子验证以及第三方账户验证。
PS:个人觉得这些流程还是比较复杂,本章内容也只是对大体的流程进行了介绍,在实际代码中还对身份验证中可能出现的其它情况进行了处理,有兴趣可通过反编译的方式查看相应类型的源码。另外感谢大家对我的支持,如有问题尽管提出,大家共同进步。(*^_^*)
参考
http://bitoftech.net/2015/01/21/asp-net-identity-2-with-asp-net-web-api-2-accounts-management/
https://www.cnblogs.com/XiongMaoMengNan/p/6785155.html
https://stackoverflow.com/questions/26166826/usecookieauthentication-vs-useexternalsignincookie
https://docs.microsoft.com/en-us/aspnet/web-api/overview/security/external-authentication-services
https://www.benday.com/2014/02/25/walkthrough-asp-net-mvc-identity-with-microsoft-account-authentication/