ASP.NET Cookie是怎麼生成的
ASP.NET Cookie是怎麼生成的
可能有人知道Cookie
的生成由machineKey
有關,machineKey
用於決定Cookie
生成的演算法和金鑰,並如果使用多臺伺服器做負載均衡時,必須指定一致的machineKey
用於解密,那麼這個過程到底是怎樣的呢?
如果需要在.NET Core
中使用ASP.NET Cookie
,本文將提到的內容也將是一些必經之路。
抽絲剝繭,一步一步分析
首先使用者通過AccountController->Login
進行登入:
// // POST: /Account/Login public async Task<ActionResult> Login(LoginViewModel model, string returnUrl) { if (!ModelState.IsValid) { return View(model); } var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false); switch (result) { case SignInStatus.Success: return RedirectToLocal(returnUrl); // ......省略其它程式碼 } }
它呼叫了SignInManager
的PasswordSignInAsync
方法,該方法程式碼如下(有刪減):
public virtual async Task<SignInStatus> PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout) { // ...省略其它程式碼 if (await UserManager.CheckPasswordAsync(user, password).WithCurrentCulture()) { if (!await IsTwoFactorEnabled(user)) { await UserManager.ResetAccessFailedCountAsync(user.Id).WithCurrentCulture(); } return await SignInOrTwoFactor(user, isPersistent).WithCurrentCulture(); } // ...省略其它程式碼 return SignInStatus.Failure; }
想瀏覽原始程式碼,可參見官方的Github
連結:https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Owin/SignInManager.cs#L235-L276
可見它先需要驗證密碼,密碼驗證正確後,它呼叫了SignInOrTwoFactor
方法,該方法程式碼如下:
private async Task<SignInStatus> SignInOrTwoFactor(TUser user, bool isPersistent) { var id = Convert.ToString(user.Id); if (await IsTwoFactorEnabled(user) && !await AuthenticationManager.TwoFactorBrowserRememberedAsync(id).WithCurrentCulture()) { var identity = new ClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id)); AuthenticationManager.SignIn(identity); return SignInStatus.RequiresVerification; } await SignInAsync(user, isPersistent, false).WithCurrentCulture(); return SignInStatus.Success; }
該程式碼只是判斷了是否需要做雙重驗證,在需要雙重驗證的情況下,它呼叫了AuthenticationManager
的SignIn
方法;否則呼叫SignInAsync
方法。SignInAsync
的原始碼如下:
public virtual async Task SignInAsync(TUser user, bool isPersistent, bool rememberBrowser)
{
var userIdentity = await CreateUserIdentityAsync(user).WithCurrentCulture();
// Clear any partial cookies from external or two factor partial sign ins
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie, DefaultAuthenticationTypes.TwoFactorCookie);
if (rememberBrowser)
{
var rememberBrowserIdentity = AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(ConvertIdToString(user.Id));
AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = isPersistent }, userIdentity, rememberBrowserIdentity);
}
else
{
AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = isPersistent }, userIdentity);
}
}
可見,最終所有的程式碼都是呼叫了AuthenticationManager.SignIn
方法,所以該方法是建立Cookie
的關鍵。
AuthenticationManager
的實現定義在Microsoft.Owin
中,因此無法在ASP.NET Identity
中找到其原始碼,因此我們開啟Microsoft.Owin
的原始碼繼續跟蹤(有刪減):
public void SignIn(AuthenticationProperties properties, params ClaimsIdentity[] identities)
{
AuthenticationResponseRevoke priorRevoke = AuthenticationResponseRevoke;
if (priorRevoke != null)
{
// ...省略不相關程式碼
AuthenticationResponseRevoke = new AuthenticationResponseRevoke(filteredSignOuts);
}
AuthenticationResponseGrant priorGrant = AuthenticationResponseGrant;
if (priorGrant == null)
{
AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(identities), properties);
}
else
{
// ...省略不相關程式碼
AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(mergedIdentities), priorGrant.Properties);
}
}
AuthenticationManager
的Github
連結如下:https://github.com/aspnet/AspNetKatana/blob/c33569969e79afd9fb4ec2d6bdff877e376821b2/src/Microsoft.Owin/Security/AuthenticationManager.cs
可見它用到了AuthenticationResponseGrant
,繼續跟蹤可以看到它實際是一個屬性:
public AuthenticationResponseGrant AuthenticationResponseGrant
{
// 省略get
set
{
if (value == null)
{
SignInEntry = null;
}
else
{
SignInEntry = Tuple.Create((IPrincipal)value.Principal, value.Properties.Dictionary);
}
}
}
發現它其實是設定了SignInEntry
,繼續追蹤:
public Tuple<IPrincipal, IDictionary<string, string>> SignInEntry
{
get { return _context.Get<Tuple<IPrincipal, IDictionary<string, string>>>(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
,核心程式碼如下:
protected override async Task ApplyResponseGrantAsync()
{
AuthenticationResponseGrant signin = Helper.LookupSignIn(Options.AuthenticationType);
// ... 省略部分程式碼
if (shouldSignin)
{
var signInContext = new CookieResponseSignInContext(
Context,
Options,
Options.AuthenticationType,
signin.Identity,
signin.Properties,
cookieOptions);
// ... 省略部分程式碼
model = new AuthenticationTicket(signInContext.Identity, signInContext.Properties);
// ... 省略部分程式碼
string cookieValue = 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
重要組成部分:
AuthenticationResponseGrant signin = Helper.LookupSignIn(Options.AuthenticationType);
繼續查閱LookupSignIn
原始碼,可看到,它就是從上文中的AuthenticationManager
中取回了AuthenticationResponseGrant
(有刪減):
public AuthenticationResponseGrant LookupSignIn(string authenticationType)
{
// ...
AuthenticationResponseGrant grant = _context.Authentication.AuthenticationResponseGrant;
// ...
foreach (var claimsIdentity in grant.Principal.Identities)
{
if (string.Equals(authenticationType, claimsIdentity.AuthenticationType, StringComparison.Ordinal))
{
return new AuthenticationResponseGrant(claimsIdentity, grant.Properties ?? new AuthenticationProperties());
}
}
return null;
}
如此一來,柳暗花明又一村,所有的線索就立即又明朗了。
Cookie的生成
從AuthenticationTicket
變成Cookie
位元組串,最關鍵的一步在這裡:
string cookieValue = Options.TicketDataFormat.Protect(model);
在接下來的程式碼中,只提到使用CookieManager
將該Cookie
位元組串新增到Http
響應中,翻閱CookieManager
可以看到如下程式碼:
public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (options == null)
{
throw new ArgumentNullException("options");
}
IHeaderDictionary responseHeaders = 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
方法即可。
逐漸明朗
該方法原始碼如下:
public string Protect(TData data)
{
byte[] userData = _serializer.Serialize(data);
byte[] protectedData = _protector.Protect(userData);
string protectedText = _encoder.Encode(protectedData);
return protectedText;
}
可見它依賴於_serializer
、_protector
、_encoder
三個類,其中,_serializer
的關鍵程式碼如下:
public virtual byte[] Serialize(AuthenticationTicket model)
{
using (var memory = new MemoryStream())
{
using (var compression = new GZipStream(memory, CompressionLevel.Optimal))
{
using (var writer = new BinaryWriter(compression))
{
Write(writer, model);
}
}
return memory.ToArray();
}
}
其本質是進行了一次二進位制序列化,並緊接著進行了gzip
壓縮,確保Cookie
大小不要失去控制(因為.NET
的二進位制序列化結果較大,並且微軟喜歡搞xml
,更大