Microsoft.AspNetCore.Authentication.Cookies從入門到精通 (二)
上篇文章我們介紹了Microsoft.AspNetCore.Authentication.Cookies
的部分內容,由於篇幅問題,不得不分多篇進行介紹,本篇我們繼續之前的介紹。
Cookie加密
在之前的Demo中,瀏覽器Cookie中的.AspNetCore.Cookies是一個加密字符串,默認Asp.Net Core是對該值做過加密處理的,至於加密方式我們不得而知(翻看源碼是可以找到一些蛛絲馬跡的), 如果我們要修改加密方式只有一種辦法就是通過DataProtectionProvider
屬性設置我們自己的實現:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
......
services.AddAuthentication()
.AddCookie(options=>{
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
options.Cookie.Expiration = TimeSpan.FromDays(365);
//設置我們自己的數據加密實現
options.DataProtectionProvider = new MyDataProtectionProvider();
})
.AddCookie("Admin","Admin",options=>{
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromDays(1);
options.Cookie.Expiration = TimeSpan.FromDays(365);
});
......
}
MyDataProtectionProvider
public class MyDataProtector : IDataProtector
{
private string _prupose;
public MyDataProtector() : this(null)
{ }
public MyDataProtector(string purpose)
{
_prupose = purpose;
}
public IDataProtector CreateProtector(string purpose)
{
_prupose = purpose;
//我們也可以在這里返回多個實例
//return new MyDataProtector(purpose);
return this;
}
public byte[] Protect(byte[] plaintext)
{
return plaintext
}
public byte[] Unprotect(byte[] protectedData)
{
return protectedData;
}
}
MyDataProtector是我們自定義的加密類,這里我們沒有實現Protect
和Unprotect
方法,只是簡單的做了一個return
操作,具體要使用什么加密方式,還是交給需要他的人吧,這里我們只演示一下如何自定義加密類就行了。
Cookie偽加密(序列化/反序列化)
偽加密說的是TicketDataFormat
屬性,上文我們介紹了IDataProtector
接口中定義的加密解密方法,這里我們介紹ISecureDataFormat
接口中也定義的Protect
和Unprotect
方法,從字面兩上看兩個接口的方法意思一樣,但是從實現代碼看,完全不是一回事,ISecureDataFormat
接口處理的並不是加密,准確的說它的作用是序列化與反序列化,我們可以看一下Asp.Net Coro中的默認實現SecureDataFormat。
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
......
services.AddAuthentication()
.AddCookie(options=>{
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
options.Cookie.Expiration = TimeSpan.FromDays(365);
options.DataProtectionProvider = new MyDataProtector();
//使用我們自己的序列化實現,這里的代碼看起來有點別扭,上一行我們已經設置MyDataProtector實例,這里又設置一次,原因是Asp.Net Core框架本身在這里設計的有點問題,如果通過依賴注入的方式就可以避免這種讓人費解的寫法。
options.TicketDataFormat =new MyTicketDataFormat(new MyDataProtector());
})
.AddCookie("Admin","Admin",options=>{
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromDays(1);
options.Cookie.Expiration = TimeSpan.FromDays(365);
});
......
}
MyTicketDataFormat
public class MyTicketDataFormat : TicketDataFormat
{
public MyTicketDataFormat(IDataProtector dataProtector):base(dataProtector)
{}
public new string Protect(AuthenticationTicket data)
{
return base.Protect(data);
}
public new string Protect(AuthenticationTicket data, string purpose)
{
return base.Protect(data, purpose);
}
public new AuthenticationTicket Unprotect(string protectedText)
{
return base.Unprotect(protectedText);
}
public new AuthenticationTicket Unprotect(string protectedText, string purpose)
{
return base.Unprotect(protectedText, purpose);
}
}
我們定義了一個序列化類MyTicketDataFormat
,這里我們繼承的父類是TicketDataFormat
,TicketDataFormat
是Asp.Net Core的默認實現,如果我們沒有設置options.TicketDataFormat
,默認使用的序列化類就是TicketDataFormat
,如果在真實的項目中你需要自定義序列化時,你應該實現ISecureDataFormat<AuthenticationTicket>
接口而不是像我這樣偷懶,我這里純粹是為了演示,我們看一下Asp.Net Core是如何實現SecureDataFormat類的(SecureDataFormat是TicketDataFormat的父類)。這里有一點需要注意,如果你實現了ISecureDataFormat<AuthenticationTicket>
接口別忘了在構造函數中注入IDataProtector
,如果你真的忘記了,那么你上文設置的options.DataProtectionProvider = new MyDataProtectionProvider();
將不會起任何作用(我認為這里是一個大坑)。
減少Cookie中Value的大小
默認情況下Asp.Net Core的實現是將AuthenticationTicket
對象序列化並加密存儲在Cookie中,那么這會有一個隱患,隨着AuthenticationTicket
對象存儲數據越來越多,Cookie也會越來越大,但是瀏覽器只給了我們4K的存儲空間,有可能在某一天你的Cookie就裝不下你的數據流,所以減少Cookie的存儲是我們或許終將考慮的一個問題;從另一個方面看,減少Cookie的數據量也利於網絡的傳輸。
那么我們怎么減小Cookie的存儲空間呢?!我們不妨借鑒一下Asp.Net Core中Session的實現原理,將Cookie信息,寫入內存、緩存服務器(redis)中,然后通過唯一標識與它對應,這樣便可以極大的減少Cookie的存儲傳輸體積。
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
......
services.AddAuthentication()
.AddCookie(options=>{
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
options.Cookie.Expiration = TimeSpan.FromDays(365);
options.DataProtectionProvider = new MyDataProtectionProvider();
options.TicketDataFormat =new MyTicketDataFormat(new MyDataProtector());
//添加Cookie存儲實現
options.SessionStore = new MyTicketStore();
})
.AddCookie("Admin","Admin",options=>{
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromDays(1);
options.Cookie.Expiration = TimeSpan.FromDays(365);
});
......
}
MyTicketStore
public class MyTicketStore : ITicketStore
{
private Dictionary<string, AuthenticationTicket> _cache = new Dictionary<string, AuthenticationTicket>();
public Task RemoveAsync(string key)
{
_cache.Remove(key);
return Task.FromResult(0);
}
public Task RenewAsync(string key, AuthenticationTicket ticket)
{
_cache[key]= ticket;
return Task.FromResult(0);
}
public Task<AuthenticationTicket> RetrieveAsync(string key)
{
_cache.TryGetValue(key, out AuthenticationTicket ticket);
return Task.FromResult(ticket);
}
public Task<string> StoreAsync(AuthenticationTicket ticket)
{
var key = Guid.NewGuid().ToString("n");
_cache.TryAdd(key, ticket);
return Task.FromResult(key);
}
}
上面的代碼我們通過實現ITicketStore
的自定義類型MyTicketStore
來自定義AuthenticationTicket
數據的存儲,在MyTicketStore
中我們通過Dictionary<string, AuthenticationTicket>
來存儲認證數據,這里其實應該使用內存,或者外部緩存系統,例如:用Redis來存儲認證數據。
自定義Cookie管理功能
我們這里說的Cookie管理指的是ICookieManager
接口,該接口主要是用來添加,刪除,獲取Cookie的信息,也就是Microsoft.AspNetCore.Authentication.Cookies
真正將Cookie寫入http頭,從http頭獲取Cookie的入口。
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
......
services.AddAuthentication()
.AddCookie(options=>{
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
options.Cookie.Expiration = TimeSpan.FromDays(365);
options.DataProtectionProvider = new MyDataProtectionProvider();
options.TicketDataFormat =new MyTicketDataFormat(new MyDataProtector());
options.SessionStore = new MyTicketStore();
options.CookieManager = new MyCookieManager();
})
.AddCookie("Admin","Admin",options=>{
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromDays(1);
options.Cookie.Expiration = TimeSpan.FromDays(365);
});
......
}
MyCookieManager
public class MyCookieManager : ICookieManager
{
public void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options)
{
context.Response.Cookies.Append(key, value, options);
}
public void DeleteCookie(HttpContext context, string key, CookieOptions options)
{
context.Response.Cookies.Delete(key, options);
}
public string GetRequestCookie(HttpContext context, string key)
{
return context.Request.Cookies[key];
}
}
我們這里的實現很簡單,僅僅調用了一下HttpContext上的對象來操作Cookie,在實現上可能會存在一些問題,更安全的實現請看ChunkingCookieManager。
總結
- 我們介紹了如何通過
IDataProtector
接口實現自己的加密方式 。 - 我們介紹了如何通過
ISecureDataFormat<AuthenticationTicket>
接口實現自己的序列化方式,並提到了這里可能有坑,當我們自定義序列化方式的時候,需要再構造函數中添加IDataProtector
參數,不然可能導致DataProtectionProvider
屬性失去意義。 - 我們介紹如何通過
ITicketStore
接口實現類似Session的存儲機制,來減少Cookie在瀏覽器中的存儲以及傳輸量。 - 我們介紹了如何通過
ICookieManager
接口實現Cookie的自定義管理功能。