天天看點

ASP.NET Cookie是怎麼生成的

ASP.NET Cookie是怎麼生成的

可能有人知道Cookie的生成由machineKey有關,machineKey用于決定Cookie生成的算法和密鑰,并如果使用多台伺服器做負載均衡時,必須指定一緻的machineKey用于解密,那麼這個過程到底是怎樣的呢?

如果需要在.NET Core中使用ASP.NET Cookie,本文将提到的内容也将是一些必經之路。

抽絲剝繭,一步一步分析

首先使用者通過AccountController->Login進行登入:

//

// POST: /Account/Login

public async Task 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 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 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> 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,更大😂)。

然後來看一下_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,
Options.AuthenticationType, "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

設定AuthenticationResponseGrant

然後進入CookieAuthentication的流程,這些代碼集中在Owin中,它會經過:

CookieAuthenticationMiddleware(讀取AuthenticationResponseGrant)

ISecureDataFormat(實作類:SecureDataFormat)

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,敬請期待!

原文位址

https://www.cnblogs.com/sdflysha/p/20200123-how-does-aspnet-identity-cookie-generated.html

繼續閱讀