ASP.NET Cookie是怎么生成的(推荐)
可能有人知道Cookie的生成由machineKey有关,machineKey用于决定Cookie生成的算法和密钥,并如果使用多台服务器做负载均衡时,必须指定一致的machineKey用于解密,那么这个过程到底是怎样的呢?
如果需要在.NETCore中使用ASP.NETCookie,本文将提到的内容也将是一些必经之路。
抽丝剥茧,一步一步分析
首先用户通过AccountController->Login进行登录:
// //POST:/Account/Login publicasyncTaskLogin(LoginViewModelmodel,stringreturnUrl) { if(!ModelState.IsValid) { returnView(model); } varresult=awaitSignInManager.PasswordSignInAsync(model.Email,model.Password,model.RememberMe,shouldLockout:false); switch(result) { caseSignInStatus.Success: returnRedirectToLocal(returnUrl); //......省略其它代码 } }
它调用了SignInManager的PasswordSignInAsync方法,该方法代码如下(有删减):
publicvirtualasyncTaskPasswordSignInAsync(stringuserName,stringpassword,boolisPersistent,boolshouldLockout) { //...省略其它代码 if(awaitUserManager.CheckPasswordAsync(user,password).WithCurrentCulture()) { if(!awaitIsTwoFactorEnabled(user)) { awaitUserManager.ResetAccessFailedCountAsync(user.Id).WithCurrentCulture(); } returnawaitSignInOrTwoFactor(user,isPersistent).WithCurrentCulture(); } //...省略其它代码 returnSignInStatus.Failure; }
想浏览原始代码,可参见官方的Github链接:
https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Owin/SignInManager.cs#L235-L276
可见它先需要验证密码,密码验证正确后,它调用了SignInOrTwoFactor方法,该方法代码如下:
privateasyncTaskSignInOrTwoFactor(TUseruser,boolisPersistent) { varid=Convert.ToString(user.Id); if(awaitIsTwoFactorEnabled(user)&&!awaitAuthenticationManager.TwoFactorBrowserRememberedAsync(id).WithCurrentCulture()) { varidentity=newClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie); identity.AddClaim(newClaim(ClaimTypes.NameIdentifier,id)); AuthenticationManager.SignIn(identity); returnSignInStatus.RequiresVerification; } awaitSignInAsync(user,isPersistent,false).WithCurrentCulture(); returnSignInStatus.Success; }
该代码只是判断了是否需要做双重验证,在需要双重验证的情况下,它调用了AuthenticationManager的SignIn方法;否则调用SignInAsync方法。SignInAsync的源代码如下:
publicvirtualasyncTaskSignInAsync(TUseruser,boolisPersistent,boolrememberBrowser)
{
varuserIdentity=awaitCreateUserIdentityAsync(user).WithCurrentCulture();
//Clearanypartialcookiesfromexternalortwofactorpartialsignins
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie,DefaultAuthenticationTypes.TwoFactorCookie);
if(rememberBrowser)
{
varrememberBrowserIdentity=AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(ConvertIdToString(user.Id));
AuthenticationManager.SignIn(newAuthenticationProperties{IsPersistent=isPersistent},userIdentity,rememberBrowserIdentity);
}
else
{
AuthenticationManager.SignIn(newAuthenticationProperties{IsPersistent=isPersistent},userIdentity);
}
}
可见,最终所有的代码都是调用了AuthenticationManager.SignIn方法,所以该方法是创建Cookie的关键。
AuthenticationManager的实现定义在Microsoft.Owin中,因此无法在ASP.NETIdentity中找到其源代码,因此我们打开Microsoft.Owin的源代码继续跟踪(有删减):
publicvoidSignIn(AuthenticationPropertiesproperties,paramsClaimsIdentity[]identities)
{
AuthenticationResponseRevokepriorRevoke=AuthenticationResponseRevoke;
if(priorRevoke!=null)
{
//...省略不相关代码
AuthenticationResponseRevoke=newAuthenticationResponseRevoke(filteredSignOuts);
}
AuthenticationResponseGrantpriorGrant=AuthenticationResponseGrant;
if(priorGrant==null)
{
AuthenticationResponseGrant=newAuthenticationResponseGrant(newClaimsPrincipal(identities),properties);
}
else
{
//...省略不相关代码
AuthenticationResponseGrant=newAuthenticationResponseGrant(newClaimsPrincipal(mergedIdentities),priorGrant.Properties);
}
}
AuthenticationManager的Github链接如下:https://github.com/aspnet/AspNetKatana/blob/c33569969e79afd9fb4ec2d6bdff877e376821b2/src/Microsoft.Owin/Security/AuthenticationManager.cs
可见它用到了AuthenticationResponseGrant,继续跟踪可以看到它实际是一个属性:
publicAuthenticationResponseGrantAuthenticationResponseGrant
{
//省略get
set
{
if(value==null)
{
SignInEntry=null;
}
else
{
SignInEntry=Tuple.Create((IPrincipal)value.Principal,value.Properties.Dictionary);
}
}
}
发现它其实是设置了SignInEntry,继续追踪:
publicTuple>SignInEntry { get{return_context.Get >>(OwinConstants.Security.SignIn);} set{_context.Set(OwinConstants.Security.SignIn,value);} }
其中,_context的类型为IOwinContext,OwinConstants.Security.SignIn的常量值为"security.SignIn"。
跟踪完毕……
啥?跟踪这么久,居然跟丢啦!?
当然没有!但接下来就需要一定的技巧了。
原来,ASP.NET是一种中间件(Middleware)模型,在这个例子中,它会先处理MVC中间件,该中间件处理流程到设置AuthenticationResponseGrant/SignInEntry为止。但接下来会继续执行CookieAuthentication中间件,该中间件的核心代码在aspnet/AspNetKatana仓库中可以看到,关键类是CookieAuthenticationHandler,核心代码如下:
protectedoverrideasyncTaskApplyResponseGrantAsync()
{
AuthenticationResponseGrantsignin=Helper.LookupSignIn(Options.AuthenticationType);
//...省略部分代码
if(shouldSignin)
{
varsignInContext=newCookieResponseSignInContext(
Context,
Options,
Options.AuthenticationType,
signin.Identity,
signin.Properties,
cookieOptions);
//...省略部分代码
model=newAuthenticationTicket(signInContext.Identity,signInContext.Properties);
//...省略部分代码
stringcookieValue=Options.TicketDataFormat.Protect(model);
Options.CookieManager.AppendResponseCookie(
Context,
Options.CookieName,
cookieValue,
signInContext.CookieOptions);
}
//...又省略部分代码
}
这个原始函数有超过200行代码,这里我省略了较多,但保留了关键、核心部分,想查阅原始代码可以移步Github链接:https://github.com/aspnet/AspNetKatana/blob/0fc4611e8b04b73f4e6bd68263e3f90e1adfa447/src/Microsoft.Owin.Security.Cookies/CookieAuthenticationHandler.cs#L130-L313
这里挑几点最重要的讲。
与MVC建立关系
建立关系的核心代码就是第一行,它从上文中提到的位置取回了AuthenticationResponseGrant,该Grant保存了Claims、AuthenticationTicket等Cookie重要组成部分:
AuthenticationResponseGrantsignin=Helper.LookupSignIn(Options.AuthenticationType);
继续查阅LookupSignIn源代码,可看到,它就是从上文中的AuthenticationManager中取回了AuthenticationResponseGrant(有删减):
publicAuthenticationResponseGrantLookupSignIn(stringauthenticationType)
{
//...
AuthenticationResponseGrantgrant=_context.Authentication.AuthenticationResponseGrant;
//...
foreach(varclaimsIdentityingrant.Principal.Identities)
{
if(string.Equals(authenticationType,claimsIdentity.AuthenticationType,StringComparison.Ordinal))
{
returnnewAuthenticationResponseGrant(claimsIdentity,grant.Properties??newAuthenticationProperties());
}
}
returnnull;
}
如此一来,柳暗花明又一村,所有的线索就立即又明朗了。
Cookie的生成
从AuthenticationTicket变成Cookie字节串,最关键的一步在这里:
stringcookieValue=Options.TicketDataFormat.Protect(model);
在接下来的代码中,只提到使用CookieManager将该Cookie字节串添加到Http响应中,翻阅CookieManager可以看到如下代码:
publicvoidAppendResponseCookie(IOwinContextcontext,stringkey,stringvalue,CookieOptionsoptions)
{
if(context==null)
{
thrownewArgumentNullException("context");
}
if(options==null)
{
thrownewArgumentNullException("options");
}
IHeaderDictionaryresponseHeaders=context.Response.Headers;
//省去“1万”行计算chunk和处理细节的流程
responseHeaders.AppendValues(Constants.Headers.SetCookie,chunks);
}
有兴趣的朋友可以访问Github看原始版本的代码:https://github.com/aspnet/AspNetKatana/blob/0fc4611e8b04b73f4e6bd68263e3f90e1adfa447/src/Microsoft.Owin/Infrastructure/ChunkingCookieManager.cs#L125-L215
可见这个实现比较……简单,就是往Response.Headers中加了个头,重点只要看TicketDataFormat.Protect方法即可。
逐渐明朗
该方法源代码如下:
publicstringProtect(TDatadata)
{
byte[]userData=_serializer.Serialize(data);
byte[]protectedData=_protector.Protect(userData);
stringprotectedText=_encoder.Encode(protectedData);
returnprotectedText;
}
可见它依赖于_serializer、_protector、_encoder三个类,其中,_serializer的关键代码如下:
publicvirtualbyte[]Serialize(AuthenticationTicketmodel)
{
using(varmemory=newMemoryStream())
{
using(varcompression=newGZipStream(memory,CompressionLevel.Optimal))
{
using(varwriter=newBinaryWriter(compression))
{
Write(writer,model);
}
}
returnmemory.ToArray();
}
}
其本质是进行了一次二进制序列化,并紧接着进行了gzip压缩,确保Cookie大小不要失去控制(因为.NET的二进制序列化结果较大,并且微软喜欢搞xml,更大