.NET跨平臺之旅:ASP.NET Core從傳統ASP.NET的Cookie中讀取用戶登陸信息

在解決了asp.net core中訪問memcached緩存的問題後,咱們開始大踏步地向.net core進軍——將更多站點向asp.net core遷移,在遷移涉及獲取用戶登陸信息的站點時,咱們遇到了一個問題——如何在asp.net core與傳統asp.net之間共享保存用戶登陸信息的cookie?html

對於cookie的加解密,傳統asp.net用的是對稱加解密算法,而asp.net core用的是基於公鑰私鑰的非對稱加解密算法,因此asp.net core沒法解密傳統asp.net生成的cookie,傳統asp.net也沒法解密asp.net core生成的cookie。針對於這個問題,.net社區已經有人提供瞭解決辦法——讓傳統asp.net改用和asp.net core同樣的加解密算法(詳見這裏),可是這須要修改全部涉及獲取用戶登陸信息的傳統asp.net站點的代碼,有些奢侈,不到萬不得已,咱們不想採用,咱們要另闢蹊徑。git

先簡化一下問題,根據咱們向ASP.NET Core遷移過渡階段的實際場景,用戶登陸操做是在傳統asp.net站點上完成的,咱們只需在asp.net core站點中解密cookie獲取用戶登陸信息便可,連加密都不須要。既然asp.net core本身解密不了,那可讓傳統asp.net幫忙解密,asp.net core將接收到的cookie經過web api發給傳統asp.net解密。簡化後問題變成了——在asp.net core中如何接收傳統asp.net的cookie?如何攔截asp.net core對cookie的解密操做?傳統asp.net如何在web api中從cookie中解密用戶驗證信息(FormsAuthenticationTicket)?在asp.net core中如何將FormsAuthenticationTicket轉換爲自身的驗證信息(AuthenticationTicket)?咱們來逐一解決這些問題。github

問題一:在asp.net core中如何接收傳統asp.net的cookie?web

這個問題很容易解決。只需在Startup.cs中將CookieAuthenticationOptions的CookieName與CookieDomain設置爲與傳統asp.net同樣。算法

var cookieOptions = new CookieAuthenticationOptions
{
    CookieName = ".CnblogsCookie",
    CookieDomain = ".cnblogs.com",
};
app.UseCookieAuthentication(cookieOptions);

問題二:如何攔截asp.net core對cookie的解密操做?api

這個問題比較棘手。咱們是經過閱讀 Microsoft.AspNetCore.Authentication.Cookies 的源碼在 CookieAuthenticationHandler.cs 中將 TicketDataFormat 揪了出來:緩存

private async Task<AuthenticateResult> ReadCookieTicket()
{
    var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName);
    //...
    var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding());
    //...
    return AuthenticateResult.Success(ticket);
}

TicketDataFormat的類型是ISecureDataFormat<AuthenticationTicket>接口,解密cookie就是調用這個接口的Unprotect方法:cookie

public interface ISecureDataFormat<TData>
{
    string Protect(TData data);
    string Protect(TData data, string purpose);
    TData Unprotect(string protectedText);
    TData Unprotect(string protectedText, string purpose);
}

並且TicketDataFormat是CookieAuthenticationOptions的一個屬性,咱們能夠直接修改這個屬性值,使用本身的ISecureDataFormat接口實現(默認實現是SecureDataFormat),在Unprotect()方法的實現中讀取protectedText參數值(這個應該就是接收到的cookie值)達到攔截目的,咱們試一下。mvc

定義一個 FormsAuthTicketDataFormat 類,實現 ISecureDataFormat<AuthenticationTicket> 接口:app

public class FormsAuthTicketDataFormat : ISecureDataFormat<AuthenticationTicket>
{
    //...

    public AuthenticationTicket Unprotect(string protectedText, string purpose)
    {
        Console.WriteLine($"{nameof(Unprotect)}(\"{protectedText}\", \"{purpose}\")");
        throw new NotImplementedException();            
    }
}

在Startup.cs中應用FormsAuthTicketDataFormat:

var cookieOptions = new CookieAuthenticationOptions
{
    //...
    TicketDataFormat = new FormsAuthTicketDataFormat()
};
app.UseCookieAuthentication(cookieOptions);

經測試驗證,接收到的的確是傳統asp.net生成的cookie值。

既然在Unprotect()方法中已經能讀取到cookie值,那緊接着就能夠將它經過web api發送給傳統asp.net解密,因而進入下一個問題。

問題三:傳統asp.net如何在web api中從cookie中解密用戶驗證信息(FormsAuthenticationTicket)?

這個問題也很好解決,只需調用 FormsAuthentication.Decrypt() 方法進行解密,並將FormsAuthenticationTicket的Name, IssueDate, Expiration三個值返回給asp.net core。

public IHttpActionResult GetTicket(string cookie)
{
    var formsAuthTicket = FormsAuthentication.Decrypt(cookie);
    return Ok(new
    {
        formsAuthTicket.Name,
        formsAuthTicket.IssueDate,
        formsAuthTicket.Expiration
    });
}

asp.net core中經過調用web api解密cookie並獲得Name, IssueDate, Expiration這三個值,因而FormsAuthTicketDataFormat.Unprotect()的實現代碼變成了這樣:

public AuthenticationTicket Unprotect(string protectedText, string purpose)
{
    var formsAuthTicket = GetFormsAuthTicket(protectedText);
    var name = formsAuthTicket.Name;
    DateTime issueDate = formsAuthTicket.IssueDate;
    DateTime expiration = formsAuthTicket.Expiration;

    throw new NotImplementedException();
}

接下來解決最後一個問題。

問題四:在asp.net core中如何將FormsAuthenticationTicket轉換爲自身的驗證信息(AuthenticationTicket)?

因爲Unprotect()方法返回參數的類型就是AuthenticationTicket,因此咱們不用換地方,繼續在這個方法中折騰。如今咱們已經有了FormsAuthenticationTicket的三個值Name, IssueDate, Expiration,咱們須要基於它們建立有效的AuthenticationTicket。

AuthenticationTicket的構造函數有3個參數,第1個參數的類型是ClaimsPrincipal,與用戶名相關聯;第2個參數的類型是AuthenticationProperties,cookie的生成時間與過時時間就存儲於其中,第3個參數authenticationScheme設置爲對應的值(這裏設置爲空字符串),代碼以下:

public AuthenticationTicket Unprotect(string protectedText, string purpose)
{
    //Get FormsAuthenticationTicket from asp.net web api
    var formsAuthTicket = GetFormsAuthTicket(protectedText);
    var name = formsAuthTicket.Name;
    DateTime issueDate = formsAuthTicket.IssueDate;
    DateTime expiration = formsAuthTicket.Expiration;

    //Create AuthenticationTicket
    var claimsIdentity = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, name) }, "Basic");
    var claimsPrincipal = new System.Security.Claims.ClaimsPrincipal(claimsIdentity);
    var authProperties = new Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties
    {
        IssuedUtc = issueDate,
        ExpiresUtc = expiration
    };
    var ticket = new AuthenticationTicket(claimsPrincipal, authProperties, _authenticationScheme);
    return ticket;
}

解決這4個問題後就大功告成了!在aps.net core mvc controller中就能顯示當前登陸用戶名,好比下面的代碼:

public IActionResult Index()
{
    return Content(User.Identity.Name);
}

完整相關實現代碼以下:

Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    var cookieOptions = new CookieAuthenticationOptions
    {
        AutomaticAuthenticate = true,
        AutomaticChallenge = true,
        CookieHttpOnly = true,
        CookieName = ".CnblogsCookie",
        CookieDomain = ".cnblogs.com",
        LoginPath = "/account/signin",
        TicketDataFormat = new FormsAuthTicketDataFormat("")
    };
    app.UseCookieAuthentication(cookieOptions);

    //...
}

FormAuthTicketDataFormat.cs

public class FormsAuthTicketDataFormat : ISecureDataFormat<AuthenticationTicket>
{
    private string _authenticationScheme;

    public FormsAuthTicketDataFormat(string authenticationScheme)
    {
        _authenticationScheme = authenticationScheme;
    }

    public AuthenticationTicket Unprotect(string protectedText, string purpose)
    {
        //Get FormsAuthenticationTicket from asp.net web api
        var formsAuthTicket = GetFormsAuthTicket(protectedText);
        var name = formsAuthTicket.Name;
        DateTime issueDate = formsAuthTicket.IssueDate;
        DateTime expiration = formsAuthTicket.Expiration;

        //Create AuthenticationTicket
        var claimsIdentity = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, name) }, "Basic");
        var claimsPrincipal = new System.Security.Claims.ClaimsPrincipal(claimsIdentity);
        var authProperties = new Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties
        {
            IssuedUtc = issueDate,
            ExpiresUtc = expiration
        };
        var ticket = new AuthenticationTicket(claimsPrincipal, authProperties, _authenticationScheme);
        return ticket;
    }

    public string Protect(AuthenticationTicket data)
    {
        throw new NotImplementedException();
    }

    public string Protect(AuthenticationTicket data, string purpose)
    {
        throw new NotImplementedException();
    }

    public AuthenticationTicket Unprotect(string protectedText)
    {
        throw new NotImplementedException();
    }

    private FormsAuthTicketDto GetFormsAuthTicket(string cookie)
    {
        return new UserService().DecryptCookie(cookie).Result;
    }
}

遺留問題:目前對[Authorize]標記不起做用。

更新:

遺留問題已解決,將

var claimsIdentity = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, name) });

改成

var claimsIdentity = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, name) }, "Basic");

也就是將authenticationType的值設爲"Basic"。

相關文章
相關標籤/搜索