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 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,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,更大😂)。
然後來看一下_encoder原始碼:
public string Encode(byte[] data) { if (data == null) { throw new ArgumentNullException("data"); } return Convert.ToBase64String(data).TrimEnd('=').Replace('+','-').Replace('/','_'); }
可見就是進行了一次簡單的base64-url編碼,注意該編碼把=號刪掉了,所以在base64-url解碼時,需要補=號。
這兩個都比較簡單,稍複雜的是_protector,它的型別是IDataProtector。
IDataProtector
它在CookieAuthenticationMiddleware中進行了初始化,建立程式碼和引數如下:
IDataProtector dataProtector = app.CreateDataProtector( typeof(CookieAuthenticationMiddleware).FullName,"v1");
注意它傳了三個引數,第一個引數是CookieAuthenticationMiddleware的FullName,也就是"Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware",第二個引數如果沒定義,預設值是CookieAuthenticationDefaults.AuthenticationType,該值為定義為"Cookies"。
但是,在預設建立的ASP.NET MVC模板專案中,該值被重新定義為ASP.NET Identity的預設值,即"ApplicationCookie",需要注意。
然後來看看CreateDataProtector的原始碼:
public static IDataProtector CreateDataProtector(this IAppBuilder app,params string[] purposes) { if (app == null) { throw new ArgumentNullException("app"); } IDataProtectionProvider dataProtectionProvider = GetDataProtectionProvider(app); if (dataProtectionProvider == null) { dataProtectionProvider = FallbackDataProtectionProvider(app); } return dataProtectionProvider.Create(purposes); } public static IDataProtectionProvider GetDataProtectionProvider(this IAppBuilder app) { if (app == null) { throw new ArgumentNullException("app"); } object value; if (app.Properties.TryGetValue("security.DataProtectionProvider",out value)) { var del = value as DataProtectionProviderDelegate; if (del != null) { return new CallDataProtectionProvider(del); } } return null; }
可見它先從IAppBuilder的"security.DataProtectionProvider"屬性中取一個IDataProtectionProvider,否則使用DpapiDataProtectionProvider。
我們翻閱程式碼,在OwinAppContext中可以看到,該值被指定為MachineKeyDataProtectionProvider:
builder.Properties[Constants.SecurityDataProtectionProvider] = new MachineKeyDataProtectionProvider().ToOwinFunction();
文中的Constants.SecurityDataProtectionProvider,剛好就被定義為"security.DataProtectionProvider"。
我們翻閱MachineKeyDataProtector的原始碼,剛好看到它依賴於MachineKey:
internal class MachineKeyDataProtector { private readonly string[] _purposes; public MachineKeyDataProtector(params string[] purposes) { _purposes = purposes; } public virtual byte[] Protect(byte[] userData) { return MachineKey.Protect(userData,_purposes); } public virtual byte[] Unprotect(byte[] protectedData) { return MachineKey.Unprotect(protectedData,_purposes); } }
最終到了我們的老朋友MachineKey。
逆推過程,破解Cookie
首先總結一下這個過程,對一個請求在Mvc中的流程來說,這些程式碼集中在ASP.NET Identity中,它會經過:
- AccountController
- SignInManager
- AuthenticationManager
設定AuthenticatinResponseGrant
然後進入CookieAuthentication的流程,這些程式碼集中在Owin中,它會經過:
CookieAuthenticationMiddleware(讀取AuthenticationResponseGrant)
ISecureDataFormat(實現類:SecureDataFormat<T>)
IDataSerializer(實現類:TicketSerializer)
IDataProtector(實現類:MachineKeyDataProtector)
ITextEncoder(實現類:Base64UrlTextEncoder)
這些過程,結果上文中找到的所有引數的值,我總結出的“祖傳破解程式碼”如下:
string cookie = "nZBqV1M-Az7yJezhb6dUzS_urj1urB0GDufSvDJSa0pv27CnDsLHRzMDdpU039j6ApL-VNfrJULfE85yU9RFzGV_aAGXHVkGckYqkCRJUKWV8SqPEjNJ5ciVzW--uxsCBNlG9jOhJI1FJIByRzYJvidjTYABWFQnSSd7XpQRjY4lb082nDZ5lwJVK3gaC_zt6H5Z1k0lUFZRb6afF52laMc___7BdZ0mZSA2kRxTk1QY8h2gQh07HqlR_p0uwTFNKi0vW9NxkplbB8zfKbfzDj7usep3zAeDEnwofyJERtboXgV9gIS21fLjc58O-4rR362IcCi2pYjaKHwZoO4LKWe1bS4r1tyzW0Ms-39Njtiyp7lRTN4HUHMUi9PxacRNgVzkfK3msTA6LkCJA3VwRm_UUeC448Lx5pkcCPCB3lGat_5ttGRjKD_lllI-YE4esXHB5eJilJDIZlEcHLv9jYhTl17H0Jl_H3FqXyPQJR-ylQfh"; var bytes = TextEncodings.Base64Url.Decode(cookie); var decrypted = MachineKey.Unprotect(bytes,"Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware","ApplicationCookie","v1"); var serializer = new TicketSerializer(); var ticket = serializer.Deserialize(decrypted); ticket.Dump(); // Dump為LINQPad專有函式,用於方便除錯顯示,此處可以用迴圈輸出代替
執行前請設定好app.config/web.config中的machineKey節點,並安裝NuGet包:Microsoft.Owin.Security,執行結果如下(完美破解):
總結
學習方式有很多種,其中看程式碼是我個人非常喜歡的一種方式,並非所有程式碼都會一馬平川。像這個例子可能還需要有一定ASP.NET知識背景。
注意這個“祖傳程式碼”是基於.NET Framework,由於其用到了MachineKey,因此無法在.NET Core中執行。我稍後將繼續深入聊聊MachineKey這個類,看它底層程式碼是如何工作的,然後最終得以在.NET Core中直接破解ASP.NET Identity中的Cookie,敬請期待!
以上所述是小編給大家介紹的ASP.NET Cookie是怎麼生成的,希望對大家有所幫助!