可能有人知道Cookie的生成由machineKey有關,machineKey用于決定Cookie生成的算法和密鑰,并如果使用多臺服務器做負載均衡時,必須指定一致的machineKey用于解密,那么這個過程到底是怎樣的呢?
如果需要在.NET Core中使用ASP.NET Cookie,本文將提到的內容也將是一些必經之路。
抽絲剝繭,一步一步分析
首先用戶通過AccountController->Login進行登錄:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// // 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方法,該方法代碼如下(有刪減):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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方法,該方法代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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的源代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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的源代碼繼續跟蹤(有刪減):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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,繼續跟蹤可以看到它實際是一個屬性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public AuthenticationResponseGrant AuthenticationResponseGrant { // 省略get set { if (value == null) { SignInEntry = null; } else { SignInEntry = Tuple.Create((IPrincipal)value.Principal, value.Properties.Dictionary); } } } |
發現它其實是設置了SignInEntry,繼續追蹤:
1
2
3
4
5
|
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,核心代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
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(有刪減):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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可以看到如下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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方法即可。
逐漸明朗
該方法源代碼如下:
1
2
3
4
5
6
7
|
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的關鍵代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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,更大