抽丝剥茧:浅议ASP.NET Cookie的生成原理

Posted DotNET技术圈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了抽丝剥茧:浅议ASP.NET Cookie的生成原理相关的知识,希望对你有一定的参考价值。

前言

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

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

抽丝剥茧,一步一步分析

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

 
   
   
 
  1. //

  2. // POST: /Account/Login

  3. public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)

  4. {

  5. if (!ModelState.IsValid)

  6. {

  7. return View(model);

  8. }


  9. switch (result)

  10. {

  11. case SignInStatus.Success:

  12. return RedirectToLocal(returnUrl);

  13. // ......省略其它代码

  14. }

  15. }

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

 
   
   
 
  1. public virtual async Task<SignInStatus> PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout)

  2. {

  3. // ...省略其它代码

  4. if (await UserManager.CheckPasswordAsync(user, password).WithCurrentCulture())

  5. {

  6. if (!await IsTwoFactorEnabled(user))

  7. {

  8. await UserManager.ResetAccessFailedCountAsync(user.Id).WithCurrentCulture();

  9. }

  10. return await SignInOrTwoFactor(user, isPersistent).WithCurrentCulture();

  11. }

  12. // ...省略其它代码

  13. return SignInStatus.Failure;

  14. }

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

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

 
   
   
 
  1. private async Task<SignInStatus> SignInOrTwoFactor(TUser user, bool isPersistent)

  2. {

  3. var id = Convert.ToString(user.Id);

  4. if (await IsTwoFactorEnabled(user) && !await AuthenticationManager.TwoFactorBrowserRememberedAsync(id).WithCurrentCulture())

  5. {

  6. var identity = new ClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie);

  7. identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id));

  8. AuthenticationManager.SignIn(identity);

  9. return SignInStatus.RequiresVerification;

  10. }

  11. await SignInAsync(user, isPersistent, false).WithCurrentCulture();

  12. return SignInStatus.Success;

  13. }

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

 
   
   
 
  1. public virtual async Task SignInAsync(TUser user, bool isPersistent, bool rememberBrowser)

  2. {

  3. var userIdentity = await CreateUserIdentityAsync(user).WithCurrentCulture();

  4. // Clear any partial cookies from external or two factor partial sign ins

  5. AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie, DefaultAuthenticationTypes.TwoFactorCookie);

  6. if (rememberBrowser)

  7. {

  8. var rememberBrowserIdentity = AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(ConvertIdToString(user.Id));

  9. AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = isPersistent }, userIdentity, rememberBrowserIdentity);

  10. }

  11. else

  12. {

  13. AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = isPersistent }, userIdentity);

  14. }

  15. }

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

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

 
   
   
 
  1. public void SignIn(AuthenticationProperties properties, params ClaimsIdentity[] identities)

  2. {

  3. AuthenticationResponseRevoke priorRevoke = AuthenticationResponseRevoke;

  4. if (priorRevoke != null)

  5. {

  6. // ...省略不相关代码

  7. AuthenticationResponseRevoke = new AuthenticationResponseRevoke(filteredSignOuts);

  8. }


  9. AuthenticationResponseGrant priorGrant = AuthenticationResponseGrant;

  10. if (priorGrant == null)

  11. {

  12. AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(identities), properties);

  13. }

  14. else

  15. {

  16. // ...省略不相关代码


  17. AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(mergedIdentities), priorGrant.Properties);

  18. }

  19. }

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

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

 
   
   
 
  1. public AuthenticationResponseGrant AuthenticationResponseGrant

  2. {

  3. // 省略get

  4. set

  5. {

  6. if (value == null)

  7. {

  8. SignInEntry = null;

  9. }

  10. else

  11. {

  12. SignInEntry = Tuple.Create((IPrincipal)value.Principal, value.Properties.Dictionary);

  13. }

  14. }

  15. }

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

 
   
   
 
  1. public Tuple<IPrincipal, IDictionary<string, string>> SignInEntry

  2. {

  3. get { return _context.Get<Tuple<IPrincipal, IDictionary<string, string>>>(OwinConstants.Security.SignIn); }

  4. set { _context.Set(OwinConstants.Security.SignIn, value); }

  5. }

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

跟踪完毕……

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

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

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

 
   
   
 
  1. protected override async Task ApplyResponseGrantAsync()

  2. {

  3. AuthenticationResponseGrant signin = Helper.LookupSignIn(Options.AuthenticationType);

  4. // ... 省略部分代码


  5. if (shouldSignin)

  6. {

  7. var signInContext = new CookieResponseSignInContext(

  8. Context,

  9. Options,

  10. Options.AuthenticationType,

  11. signin.Identity,

  12. signin.Properties,

  13. cookieOptions);


  14. // ... 省略部分代码


  15. model = new AuthenticationTicket(signInContext.Identity, signInContext.Properties);

  16. // ... 省略部分代码


  17. string cookieValue = Options.TicketDataFormat.Protect(model);


  18. Options.CookieManager.AppendResponseCookie(

  19. Context,

  20. Options.CookieName,

  21. cookieValue,

  22. signInContext.CookieOptions);

  23. }

  24. // ... 又省略部分代码

  25. }

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

这里挑几点最重要的讲。

与 MVC建立关系

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

 
   
   
 
  1. AuthenticationResponseGrant signin = Helper.LookupSignIn(Options.AuthenticationType);

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

 
   
   
 
  1. public AuthenticationResponseGrant LookupSignIn(string authenticationType)

  2. {

  3. // ...

  4. AuthenticationResponseGrant grant = _context.Authentication.AuthenticationResponseGrant;

  5. // ...


  6. foreach (var claimsIdentity in grant.Principal.Identities)

  7. {

  8. if (string.Equals(authenticationType, claimsIdentity.AuthenticationType, StringComparison.Ordinal))

  9. {

  10. return new AuthenticationResponseGrant(claimsIdentity, grant.Properties ?? new AuthenticationProperties());

  11. }

  12. }


  13. return null;

  14. }

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

Cookie的生成

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

 
   
   
 
  1. string cookieValue = Options.TicketDataFormat.Protect(model);

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

 
   
   
 
  1. public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)

  2. {

  3. if (context == null)

  4. {

  5. throw new ArgumentNullException("context");

  6. }

  7. if (options == null)

  8. {

  9. throw new ArgumentNullException("options");

  10. }


  11. IHeaderDictionary responseHeaders = context.Response.Headers;

  12. // 省去“1万”行计算chunk和处理细节的流程

  13. responseHeaders.AppendValues(Constants.Headers.SetCookie, chunks);

  14. }

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

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

逐渐明朗

该方法源代码如下:

 
   
   
 
  1. public string Protect(TData data)

  2. {

  3. byte[] userData = _serializer.Serialize(data);

  4. byte[] protectedData = _protector.Protect(userData);

  5. string protectedText = _encoder.Encode(protectedData);

  6. return protectedText;

  7. }

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

 
   
   
 
  1. public virtual byte[] Serialize(AuthenticationTicket model)

  2. {

  3. using (var memory = new MemoryStream())

  4. {

  5. using (var compression = new GZipStream(memory, CompressionLevel.Optimal))

  6. {

  7. using (var writer = new BinaryWriter(compression))

  8. {

  9. Write(writer, model);

  10. }

  11. }

  12. return memory.ToArray();

  13. }

  14. }

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

以上是关于抽丝剥茧:浅议ASP.NET Cookie的生成原理的主要内容,如果未能解决你的问题,请参考以下文章

无法在 ASP.NET Core 和 React 中创建包含 jwt 的 httponly cookie

Asp.net 核心中的会话和应用程序变量,以防我们使用 JWT 令牌(因此没有基于 cookie 的会话)

asp.net对cookie的操作

ASP.NET与ASP.NET Core用户验证Cookie并存解决方案

ASP.NET-后台cookie与前台JQUERY解析cookie

ASP.NET,Cookie,写Cookie,取Cookie