在解決了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"。
文章列表