文章出處

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

對于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站點的代碼,有些奢侈,不到萬不得已,我們不想采用,我們要另辟蹊徑。

先簡化一下問題,根據我們向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)?我們來逐一解決這些問題。

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

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

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

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

這個問題比較棘手。我們是通過閱讀 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方法:

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值)達到攔截目的,我們試一下。

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

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"。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()