1. 程式人生 > >ASP.NET Cookie是怎麼生成的

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);
        // ......省略其它程式碼
    }
}

它呼叫了SignInManagerPasswordSignInAsync方法,該方法程式碼如下(有刪減):

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;
}

該程式碼只是判斷了是否需要做雙重驗證,在需要雙重驗證的情況下,它呼叫了AuthenticationManagerSignIn方法;否則呼叫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);
    }
}

AuthenticationManagerGithub連結如下: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的型別為IOwinContextOwinConstants.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儲存了ClaimsAuthenticationTicketCookie重要組成部分:

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,更大