ASP.NET Cookie是怎么生成的

前言

可能有人知道 Cookie 的生成由  machineKey 有关,  machineKey 用于决定  Cookie 生成的算法和密钥,并如果使用多台服务器做负载均衡时,必须指定一致的  machineKey 用于解密,那么这个过程到底是怎样的呢?

如果需要在 .NETCore 中使用  ASP.NETCookie ,本文将提到的内容也将是一些必经之路。

抽丝剥茧,一步一步分析

首先用户通过 AccountController->Login 进行登录:

它调用了 SignInManager 的  PasswordSignInAsync 方法,该方法代码如下(有删减):

想浏览原始代码,可参见官方的 Github 链接:https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Owin/SignInManager.cs#L235-L276

可见它先需要验证密码,密码验证正确后,它调用了 SignInOrTwoFactor 方法,该方法代码如下:

该代码只是判断了是否需要做双重验证,在需要双重验证的情况下,它调用了 AuthenticationManager 的  SignIn 方法;否则调用  SignInAsync 方法。  SignInAsync 的源代码如下:

可见,最终所有的代码都是调用了 AuthenticationManager.SignIn 方法,所以该方法是创建  Cookie 的关键。

AuthenticationManager 的实现定义在  Microsoft.Owin 中,因此无法在  ASP.NETIdentity 中找到其源代码,因此我们打开  Microsoft.Owin 的源代码继续跟踪(有删减):

AuthenticationManager 的  Github 链接如下:https://github.com/aspnet/AspNetKatana/blob/c33569969e79afd9fb4ec2d6bdff877e376821b2/src/Microsoft.Owin/Security/AuthenticationManager.cs

可见它用到了 AuthenticationResponseGrant ,继续跟踪可以看到它实际是一个属性:

发现它其实是设置了 SignInEntry ,继续追踪:

其中, _context 的类型为  IOwinContext ,  OwinConstants.Security.SignIn 的常量值为  "security.SignIn"

跟踪完毕……

啥?跟踪这么久,居然跟丢啦!?

当然没有!但接下来就需要一定的技巧了。

原来, ASP.NET 是一种中间件(  Middleware )模型,在这个例子中,它会先处理  MVC 中间件,该中间件处理流程到设置  AuthenticationResponseGrantSignInEntry 为止。但接下来会继续执行  CookieAuthentication 中间件,该中间件的核心代码在  aspnet/AspNetKatana 仓库中可以看到,关键类是  CookieAuthenticationHandler ,核心代码如下:

这个原始函数有超过 200 行代码,这里我省略了较多,但保留了关键、核心部分,想查阅原始代码可以移步  Github 链接:https://github.com/aspnet/AspNetKatana/blob/0fc4611e8b04b73f4e6bd68263e3f90e1adfa447/src/Microsoft.Owin.Security.Cookies/CookieAuthenticationHandler.cs#L130-L313

这里挑几点最重要的讲。

MVC 建立关系

建立关系的核心代码就是第一行,它从上文中提到的位置取回了 AuthenticationResponseGrant ,该  Grant 保存了  Claims 、  AuthenticationTicket 等  Cookie 重要组成部分:

继续查阅 LookupSignIn 源代码,可看到,它就是从上文中的  AuthenticationManager 中取回了  AuthenticationResponseGrant (有删减):

如此一来,柳暗花明又一村,所有的线索就立即又明朗了。

Cookie的生成

AuthenticationTicket 变成  Cookie 字节串,最关键的一步在这里:

在接下来的代码中,只提到使用 CookieManager 将该  Cookie 字节串添加到  Http 响应中,翻阅  CookieManager 可以看到如下代码:

有兴趣的朋友可以访问 Github 看原始版本的代码:https://github.com/aspnet/AspNetKatana/blob/0fc4611e8b04b73f4e6bd68263e3f90e1adfa447/src/Microsoft.Owin/Infrastructure/ChunkingCookieManager.cs#L125-L215

可见这个实现比较……简单,就是往 Response.Headers 中加了个头,重点只要看  TicketDataFormat.Protect 方法即可。

逐渐明朗

该方法源代码如下:

可见它依赖于 _serializer 、  _protector 、  _encoder 三个类,其中,  _serializer 的关键代码如下:

其本质是进行了一次二进制序列化,并紧接着进行了 gzip 压缩,确保  Cookie 大小不要失去控制(因为  .NET 的二进制序列化结果较大,并且微软喜欢搞  xml ,更大:joy:)。

然后来看一下 _encoder 源代码:

可见就是进行了一次简单的 base64-url 编码,注意该编码把  = 号删掉了,所以在  base64-url 解码时,需要补  = 号。

这两个都比较简单,稍复杂的是 _protector ,它的类型是  IDataProtector

IDataProtector

它在 CookieAuthenticationMiddleware 中进行了初始化,创建代码和参数如下:

注意它传了三个参数,第一个参数是 CookieAuthenticationMiddleware 的  FullName ,也就是  "Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware" ,第二个参数如果没定义,默认值是  CookieAuthenticationDefaults.AuthenticationType ,该值为定义为  "Cookies"

但是,在默认创建的 ASP.NET MVC 模板项目中,该值被重新定义为  ASP.NETIdentity 的默认值,即  "ApplicationCookie" ,需要注意。

然后来看看 CreateDataProtector 的源码:

可见它先从 IAppBuilder 的  "security.DataProtectionProvider" 属性中取一个  IDataProtectionProvider ,否则使用  DpapiDataProtectionProvider

我们翻阅代码,在 OwinAppContext 中可以看到,该值被指定为  MachineKeyDataProtectionProvider

文中的 Constants.SecurityDataProtectionProvider ,刚好就被定义为  "security.DataProtectionProvider"

我们翻阅 MachineKeyDataProtector 的源代码,刚好看到它依赖于  MachineKey

最终到了我们的老朋友 MachineKey

逆推过程,破解Cookie

首先总结一下这个过程,对一个请求在 Mvc 中的流程来说,这些代码集中在  ASP.NETIdentity 中,它会经过:

  1. AccountController

  2. SignInManager

  3. AuthenticationManager

  4. 设置  AuthenticationResponseGrant

然后进入 CookieAuthentication 的流程,这些代码集中在  Owin 中,它会经过:

  1. CookieAuthenticationMiddleware (读取  AuthenticationResponseGrant

  2. ISecureDataFormat (实现类:  SecureDataFormat<T>

  3. IDataSerializer (实现类:  TicketSerializer

  4. IDataProtector (实现类:  MachineKeyDataProtector

  5. ITextEncoder (实现类:  Base64UrlTextEncoder

这些过程,结果上文中找到的所有参数的值,我总结出的“祖传破解代码”如下:

运行前请设置好 app.configweb.config 中的  machineKey 节点,并安装  NuGet 包:  Microsoft.Owin.Security ,运行结果如下(完美破解): 

学习方式有很多种,其中看代码是我个人非常喜欢的一种方式,并非所有代码都会一马平川。像这个例子可能还需要有一定 ASP.NET 知识背景。

注意这个“祖传代码”是基于 .NETFramework ,由于其用到了  MachineKey ,因此无法在  .NETCore 中运行。我稍后将继续深入聊聊  MachineKey 这个类,看它底层代码是如何工作的,然后最终得以在  .NETCore 中直接破解  ASP.NETIdentity 中的  Cookie ,敬请期待!

最后,在新的一年里,祝大家阖家欢乐,鼠年大吉!