ASP.NET Cookie是怎么生成的(推薦)
可能有人知道Cookie的生成由machineKey有關(guān),machineKey用于決定Cookie生成的算法和密鑰,并如果使用多臺(tái)服務(wù)器做負(fù)載均衡時(shí),必須指定一致的machineKey用于解密,那么這個(gè)過(guò)程到底是怎樣的呢?
如果需要在.NET Core中使用ASP.NET Cookie,本文將提到的內(nèi)容也將是一些必經(jīng)之路。
抽絲剝繭,一步一步分析
首先用戶通過(guò)AccountController->Login進(jìn)行登錄:
//
// 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);
// ......省略其它代碼
}
}
它調(diào)用了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;
}
想瀏覽原始代碼,可參見(jiàn)官方的Github鏈接:
可見(jiàn)它先需要驗(yàn)證密碼,密碼驗(yàn)證正確后,它調(diào)用了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;
}
該代碼只是判斷了是否需要做雙重驗(yàn)證,在需要雙重驗(yàn)證的情況下,它調(diào)用了AuthenticationManager的SignIn方法;否則調(diào)用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);
}
}
可見(jiàn),最終所有的代碼都是調(diào)用了AuthenticationManager.SignIn方法,所以該方法是創(chuàng)建Cookie的關(guān)鍵。
AuthenticationManager的實(shí)現(xiàn)定義在Microsoft.Owin中,因此無(wú)法在ASP.NET Identity中找到其源代碼,因此我們打開(kāi)Microsoft.Owin的源代碼繼續(xù)跟蹤(有刪減):
public void SignIn(AuthenticationProperties properties, params ClaimsIdentity[] identities)
{
AuthenticationResponseRevoke priorRevoke = AuthenticationResponseRevoke;
if (priorRevoke != null)
{
// ...省略不相關(guān)代碼
AuthenticationResponseRevoke = new AuthenticationResponseRevoke(filteredSignOuts);
}
AuthenticationResponseGrant priorGrant = AuthenticationResponseGrant;
if (priorGrant == null)
{
AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(identities), properties);
}
else
{
// ...省略不相關(guān)代碼
AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(mergedIdentities), priorGrant.Properties);
}
}
AuthenticationManager的Github鏈接如下:https://github.com/aspnet/AspNetKatana/blob/c33569969e79afd9fb4ec2d6bdff877e376821b2/src/Microsoft.Owin/Security/AuthenticationManager.cs
可見(jiàn)它用到了AuthenticationResponseGrant,繼續(xù)跟蹤可以看到它實(shí)際是一個(gè)屬性:
public AuthenticationResponseGrant AuthenticationResponseGrant
{
// 省略get
set
{
if (value == null)
{
SignInEntry = null;
}
else
{
SignInEntry = Tuple.Create((IPrincipal)value.Principal, value.Properties.Dictionary);
}
}
}
發(fā)現(xiàn)它其實(shí)是設(shè)置了SignInEntry,繼續(xù)追蹤:
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"。
跟蹤完畢……
啥?跟蹤這么久,居然跟丟啦???
當(dāng)然沒(méi)有!但接下來(lái)就需要一定的技巧了。
原來(lái),ASP.NET是一種中間件(Middleware)模型,在這個(gè)例子中,它會(huì)先處理MVC中間件,該中間件處理流程到設(shè)置AuthenticationResponseGrant/SignInEntry為止。但接下來(lái)會(huì)繼續(xù)執(zhí)行CookieAuthentication中間件,該中間件的核心代碼在aspnet/AspNetKatana倉(cāng)庫(kù)中可以看到,關(guān)鍵類是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);
}
// ... 又省略部分代碼
}
這個(gè)原始函數(shù)有超過(guò)200行代碼,這里我省略了較多,但保留了關(guān)鍵、核心部分,想查閱原始代碼可以移步Github鏈接:https://github.com/aspnet/AspNetKatana/blob/0fc4611e8b04b73f4e6bd68263e3f90e1adfa447/src/Microsoft.Owin.Security.Cookies/CookieAuthenticationHandler.cs#L130-L313
這里挑幾點(diǎn)最重要的講。
與MVC建立關(guān)系
建立關(guān)系的核心代碼就是第一行,它從上文中提到的位置取回了AuthenticationResponseGrant,該Grant保存了Claims、AuthenticationTicket等Cookie重要組成部分:
AuthenticationResponseGrant signin = Helper.LookupSignIn(Options.AuthenticationType);
繼續(xù)查閱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;
}
如此一來(lái),柳暗花明又一村,所有的線索就立即又明朗了。
Cookie的生成
從AuthenticationTicket變成Cookie字節(jié)串,最關(guān)鍵的一步在這里:
string cookieValue = Options.TicketDataFormat.Protect(model);
在接下來(lái)的代碼中,只提到使用CookieManager將該Cookie字節(jié)串添加到Http響應(yīng)中,翻閱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萬(wàn)”行計(jì)算chunk和處理細(xì)節(jié)的流程
responseHeaders.AppendValues(Constants.Headers.SetCookie, chunks);
}
有興趣的朋友可以訪問(wèn)Github看原始版本的代碼:https://github.com/aspnet/AspNetKatana/blob/0fc4611e8b04b73f4e6bd68263e3f90e1adfa447/src/Microsoft.Owin/Infrastructure/ChunkingCookieManager.cs#L125-L215
可見(jiàn)這個(gè)實(shí)現(xiàn)比較……簡(jiǎn)單,就是往Response.Headers中加了個(gè)頭,重點(diǎn)只要看TicketDataFormat.Protect方法即可。
逐漸明朗
該方法源代碼如下:
public string Protect(TData data)
{
byte[] userData = _serializer.Serialize(data);
byte[] protectedData = _protector.Protect(userData);
string protectedText = _encoder.Encode(protectedData);
return protectedText;
}
可見(jiàn)它依賴于_serializer、_protector、_encoder三個(gè)類,其中,_serializer的關(guān)鍵代碼如下:
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();
}
}
其本質(zhì)是進(jìn)行了一次二進(jìn)制序列化,并緊接著進(jìn)行了gzip壓縮,確保Cookie大小不要失去控制(因?yàn)?NET的二進(jìn)制序列化結(jié)果較大,并且微軟喜歡搞xml,更大😂)。
然后來(lái)看一下_encoder源代碼:
public string Encode(byte[] data)
{
if (data == null)
{
throw new ArgumentNullException("data");
}
return Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
可見(jiàn)就是進(jìn)行了一次簡(jiǎn)單的base64-url編碼,注意該編碼把=號(hào)刪掉了,所以在base64-url解碼時(shí),需要補(bǔ)=號(hào)。
這兩個(gè)都比較簡(jiǎn)單,稍復(fù)雜的是_protector,它的類型是IDataProtector。
IDataProtector
它在CookieAuthenticationMiddleware中進(jìn)行了初始化,創(chuàng)建代碼和參數(shù)如下:
IDataProtector dataProtector = app.CreateDataProtector( typeof(CookieAuthenticationMiddleware).FullName, Options.AuthenticationType, "v1");
注意它傳了三個(gè)參數(shù),第一個(gè)參數(shù)是CookieAuthenticationMiddleware的FullName,也就是"Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware",第二個(gè)參數(shù)如果沒(méi)定義,默認(rèn)值是CookieAuthenticationDefaults.AuthenticationType,該值為定義為"Cookies"。
但是,在默認(rèn)創(chuàng)建的ASP.NET MVC模板項(xiàng)目中,該值被重新定義為ASP.NET Identity的默認(rèn)值,即"ApplicationCookie",需要注意。
然后來(lái)看看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;
}
可見(jiàn)它先從IAppBuilder的"security.DataProtectionProvider"屬性中取一個(gè)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。
逆推過(guò)程,破解Cookie
首先總結(jié)一下這個(gè)過(guò)程,對(duì)一個(gè)請(qǐng)求在Mvc中的流程來(lái)說(shuō),這些代碼集中在ASP.NET Identity中,它會(huì)經(jīng)過(guò):
- AccountController
- SignInManager
- AuthenticationManager
設(shè)置AuthenticatinResponseGrant
然后進(jìn)入CookieAuthentication的流程,這些代碼集中在Owin中,它會(huì)經(jīng)過(guò):
CookieAuthenticationMiddleware(讀取AuthenticationResponseGrant)
ISecureDataFormat(實(shí)現(xiàn)類:SecureDataFormat<T>)
IDataSerializer(實(shí)現(xiàn)類:TicketSerializer)
IDataProtector(實(shí)現(xiàn)類:MachineKeyDataProtector)
ITextEncoder(實(shí)現(xiàn)類:Base64UrlTextEncoder)
這些過(guò)程,結(jié)果上文中找到的所有參數(shù)的值,我總結(jié)出的“祖?zhèn)髌平獯a”如下:
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為L(zhǎng)INQPad專有函數(shù),用于方便調(diào)試顯示,此處可以用循環(huán)輸出代替
運(yùn)行前請(qǐng)?jiān)O(shè)置好app.config/web.config中的machineKey節(jié)點(diǎn),并安裝NuGet包:Microsoft.Owin.Security,運(yùn)行結(jié)果如下(完美破解):

總結(jié)
學(xué)習(xí)方式有很多種,其中看代碼是我個(gè)人非常喜歡的一種方式,并非所有代碼都會(huì)一馬平川。像這個(gè)例子可能還需要有一定ASP.NET知識(shí)背景。
注意這個(gè)“祖?zhèn)鞔a”是基于.NET Framework,由于其用到了MachineKey,因此無(wú)法在.NET Core中運(yùn)行。我稍后將繼續(xù)深入聊聊MachineKey這個(gè)類,看它底層代碼是如何工作的,然后最終得以在.NET Core中直接破解ASP.NET Identity中的Cookie,敬請(qǐng)期待!
以上所述是小編給大家介紹的ASP.NET Cookie是怎么生成的,希望對(duì)大家有所幫助!
相關(guān)文章
解決WPF中空域問(wèn)題(Airspace issuse)
空域問(wèn)題是由于Winform與WPF在底層渲染機(jī)制上有所區(qū)別而導(dǎo)致的。下面小編給大家分享解決WPF中空域問(wèn)題(Airspace issuse),需要的朋友參考下吧2017-04-04
ASP.NET MVC @Helper輔助方法和@functons自定義函數(shù)的使用方法
本文主要介紹ASP.NET MVC中使用@Helper和@functons自定義一些代碼片段,方便視圖調(diào)用,從而達(dá)到減少重復(fù)代碼,快速開(kāi)發(fā)的目的,希望對(duì)大家有所幫助。2016-04-04
ASP.net中網(wǎng)站訪問(wèn)量統(tǒng)計(jì)方法代碼
這篇文章介紹了ASP.net中網(wǎng)站訪問(wèn)量統(tǒng)計(jì)方法代碼,有需要的朋友可以參考一下2013-11-11
asp.net簡(jiǎn)單實(shí)現(xiàn)單點(diǎn)登錄(SSO)的方法
這篇文章主要介紹了asp.net簡(jiǎn)單實(shí)現(xiàn)單點(diǎn)登錄(SSO)的方法,結(jié)合簡(jiǎn)單實(shí)例形式較為詳細(xì)的分析了單點(diǎn)登錄的原理與asp.net的相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2015-12-12
VS2010發(fā)布Web網(wǎng)站技術(shù)攻略
本篇文章主要包含了完整的發(fā)布網(wǎng)站步驟、發(fā)布網(wǎng)站過(guò)程中可能遇到的問(wèn)題,以及配套的解決方法,相信感興趣的朋友一定會(huì)喜歡這篇文章的2015-07-07

