來源:https://www.cnblogs.com/RainingNight/p/cookie-authentication-in-asp-net-core.html css
因爲HTTP協議是無狀態的,但對於認證來講,必然要經過一種機制來保存用戶狀態,而最經常使用,也最簡單的就是Cookie了,它由瀏覽器自動保存並在發送請求時自動附加到請求頭中。儘管在現代Web應用中,Cookie已略顯笨重,但它依然是最爲重要的用戶身份保存方式。在 上一章 中總體的介紹了一下 ASP.NET Core 中的認證流程,而未說起具體的實現方式,較爲抽象,那本章就經過一個完整的示例,以及對其原理的解剖,來詳細介紹一下Cookie認證,但願能幫助你們對 ASP.NET Core 認證系統有一個更深刻的瞭解。html
目錄react
咱們從零開始,一步一步來建立一個完整的 ASP.NET Core Cookie認證的詳細示例。git
咱們首先建立一個空的 .NET Core 2.0 Web 項目:github
在建立的空項目中,默認具備以下引用:shell
<ItemGroup> <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" /> </ItemGroup>
Microsoft.AspNetCore.All
是 ASP.NET Core 的全家桶,包含 Mvc, EFCore, Identity, NodeService, AzureAppServe 等等,並集成到了 .NET Core SDK 當中,我的感受略坑,有違 .NET Core 輕量模塊化的理念,雖然使用看似更加方便了,卻也讓咱們變得更加傻瓜化。在本文中爲了更好的演示,就移除掉 Microsoft.AspNetCore.All 的引用,而後手動安裝須要的Nuget包,固然,你能夠不這麼作,並略過此小節。數據庫
在本章中,會用到如下幾個項目中的Nuget包:json
HttpSysServer
或IISIntegration
。Cookies
, JwtBearer
, OAuth
, OpenIdConnect
等。咱們使用 DotNet CLI 來安裝Nuget包:bootstrap
dotnet add package Microsoft.AspNetCore.Hosting --version 2.0.0 dotnet add package Microsoft.AspNetCore.Server.Kestrel --version 2.0.0 dotnet add package Microsoft.Extensions.Logging.Console --version 2.0.0 dotnet add package Microsoft.AspNetCore.Authentication.Cookies --version 2.0.0
因爲WebHost.CreateDefaultBuilder
包含在Microsoft.AspNetCore.All
中,須要對Program
作以下修改:瀏覽器
public static IWebHost BuildWebHost(string[] args) => new WebHostBuilder() .UseKestrel() .UseUrls("http://localhost:5000") .UseContentRoot(Directory.GetCurrentDirectory()) .ConfigureLogging((hostingContext, logging) => { logging.AddConsole(); }) .UseStartup<Startup>() .Build();
如上,咱們使用Kestrel
服務器,監聽5000
端口,並將日誌打印到控制檯,須要注意的是咱們並無使用UseIISIntegration
,所以不支持在IIS下運行,須要使用控制檯的方式來運行,修改Properties/launchSettings.json
文件:
{
"profiles": { "Console": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } }
如上,將IIS的相關配置刪除掉,只保留控制檯的啓動配置,而後在Starup
文件的Configure
方法中添加以下代碼:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); }
最後按下F5,啓動程序,在瀏覽器中訪問訪問:http://localhost:5000/,輸出:
Hello World!
在 ASP.NET Core 中,有一個很是重要的依賴注入系統,它貫穿於全部項目中。對於認證系統,一樣要先進行註冊:
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) .AddCookie(options => { // 在這裏能夠根據須要添加一些Cookie認證相關的配置,在本次示例中使用默認值就能夠了。 }); }
如上,咱們只配置了DefaultScheme
,這樣,DefaultSignInScheme, DefaultSignOutScheme, DefaultChallengeScheme, DefaultForbidScheme 等都會使用該 Scheme 做爲默認值。
AddCookie 用來註冊 CookieAuthenticationHandler
,由它來完成身份認證的主要邏輯。
在註冊完服務以後,接下來即是註冊中間件,在 ASP.NET Core 中都是這個套路:
public void Configure(IApplicationBuilder app) { app.UseAuthentication(); }
如上,使用UseAuthentication方法註冊了AuthenticationMiddleware
中間件,它會負責調用對應的Handler,在上一章中有詳細的介紹。
既然是身份認證,那首先要有用戶,咱們在這裏模擬一個用戶倉儲,用來實現用戶登陸時的用戶名和密碼的檢查。
定義用戶類:
public class User { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } public string PhoneNumber { get; set; } public string Password { get; set; } public DateTime Birthday { get; set; } }
定義用戶倉儲:
public class UserStore { private static List<User> _users = new List<User>() { new User { Id=1, Name="alice", Password="alice", Email="alice@gmail.com", PhoneNumber="18800000001" }, new User { Id=1, Name="bob", Password="bob", Email="bob@gmail.com", PhoneNumber="18800000002" } }; public User FindUser(string userName, string password) { return _users.FirstOrDefault(_ => _.Name == userName && _.Password == password); } }
將UserStore
註冊到DI系統中:
public void ConfigureServices(IServiceCollection services) { ... services.AddSingleton<UserStore>(); }
因爲咱們並無使用MVC,而使用字符串拼接的形式返回HTML較爲費勁,在這裏定義幾個生成HTML的擴展方法:
public static class HttpResponseExtensions { public static async Task WriteHtmlAsync(this HttpResponse response, Func<HttpResponse, Task> writeContent) { var bootstrap = "<link rel=\"stylesheet\" href=\"https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css\" integrity=\"sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u\" crossorigin=\"anonymous\">"; response.ContentType = "text/html"; await response.WriteAsync($"<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"UTF-8\">{bootstrap}</head><body><div class=\"container\">"); await writeContent(response); await response.WriteAsync("</div></body></html>"); } public static async Task WriteTableHeader(this HttpResponse response, IEnumerable<string> columns, IEnumerable<IEnumerable<string>> data) { await response.WriteAsync("<table class=\"table table-condensed\">"); await response.WriteAsync("<tr>"); foreach (var column in columns) { await response.WriteAsync($"<th>{HtmlEncode(column)}</th>"); } await response.WriteAsync("</tr>"); foreach (var row in data) { await response.WriteAsync("<tr>"); foreach (var column in row) { await response.WriteAsync($"<td>{HtmlEncode(column)}</td>"); } await response.WriteAsync("</tr>"); } await response.WriteAsync("</table>"); } public static string HtmlEncode(string content) => string.IsNullOrEmpty(content) ? string.Empty : HtmlEncoder.Default.Encode(content); }
接下來,即可以在咱們的應用程序中愉快的使用認證系統了。在本文中只是最簡單的演示,便不使用MVC了,而是在Configure
中經過中間件的形式來實現。
首先,咱們定義一個登陸的頁面以及登陸成功後身體令牌的發放:
app.Map("/Account/Login", builder => builder.Run(async context => { if (context.Request.Method == "GET") { await context.Response.WriteHtmlAsync(async res => { await res.WriteAsync($"<form method=\"post\">"); await res.WriteAsync($"<input type=\"hidden\" name=\"returnUrl\" value=\"{HttpResponseExtensions.HtmlEncode(context.Request.Query["ReturnUrl"])}\"/>"); await res.WriteAsync($"<div class=\"form-group\"><label>用戶名:<input type=\"text\" name=\"userName\" class=\"form-control\"></label></div>"); await res.WriteAsync($"<div class=\"form-group\"><label>密碼:<input type=\"password\" name=\"password\" class=\"form-control\"></label></div>"); await res.WriteAsync($"<button type=\"submit\" class=\"btn btn-default\">登陸</button>"); await res.WriteAsync($"</form>"); }); } else { var userStore = context.RequestServices.GetService<UserStore>(); var user = userStore.FindUser(context.Request.Form["userName"], context.Request.Form["password"]); if (user == null) { await context.Response.WriteHtmlAsync(async res => { await res.WriteAsync($"<h1>用戶名或密碼錯誤。</h1>"); await res.WriteAsync("<a class=\"btn btn-default\" href=\"/Account/Login\">返回</a>"); }); } else { var claimIdentity = new ClaimsIdentity("Cookie"); claimIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())); claimIdentity.AddClaim(new Claim(ClaimTypes.Name, user.Name)); claimIdentity.AddClaim(new Claim(ClaimTypes.Email, user.Email)); claimIdentity.AddClaim(new Claim(ClaimTypes.MobilePhone, user.PhoneNumber)); claimIdentity.AddClaim(new Claim(ClaimTypes.DateOfBirth, user.Birthday.ToString())); var claimsPrincipal = new ClaimsPrincipal(claimIdentity); // 在上面註冊AddAuthentication時,指定了默認的Scheme,在這裏即可以再也不指定Scheme。 await context.SignInAsync(claimsPrincipal); if (string.IsNullOrEmpty(context.Request.Form["ReturnUrl"])) context.Response.Redirect("/"); else context.Response.Redirect(context.Request.Form["ReturnUrl"]); } } }));
如上,咱們在Get請求中返回登陸頁面,在Post請求中驗證用戶名密碼,匹配成功後,建立用戶Claim, ClaimsIdentity, ClaimsPrincipal 最終經過SignInAsync
方法將用戶身份寫入到響應Cookie中,完成身份令牌的發放。
咱們在登陸中間件後面添加一個自定義的受權中間件,用來禁用匿名用戶的訪問:
app.UseAuthorize();
UseAuthorize的實現很簡單,就是判斷用戶是否已經過認證,並跳過對首頁的驗證:
public static IApplicationBuilder UseAuthorize(this IApplicationBuilder app) { return app.Use(async (context, next) => { if (context.Request.Path == "/") { await next(); } else { var user = context.User; if (user?.Identity?.IsAuthenticated ?? false) { await next(); } else { await context.ChallengeAsync(); } } }); }
其實上面的實現和咱們在MVC5中經常使用的[Authorize]
特性很是類似。
再定義一個認證後才能訪問的頁面,並把當前登陸用戶的信息展現出來:
app.Map("/profile", builder => builder.Run(async context => { await context.Response.WriteHtmlAsync(async res => { await res.WriteAsync($"<h1>你好,當前登陸用戶: {HttpResponseExtensions.HtmlEncode(context.User.Identity.Name)}</h1>"); await res.WriteAsync("<a class=\"btn btn-default\" href=\"/Account/Logout\">退出</a>"); await res.WriteAsync($"<h2>AuthenticationType:{context.User.Identity.AuthenticationType}</h2>"); await res.WriteAsync("<h2>Claims:</h2>"); await res.WriteTableHeader(new string[] { "Claim Type", "Value" }, context.User.Claims.Select(c => new string[] { c.Type, c.Value })); }); }));
退出則直接調用SignOutAsync
方法便可:
app.Map("/Account/Logout", builder => builder.Run(async context => { await context.SignOutAsync(); context.Response.Redirect("/"); }));
最後,添加一個簡單的首頁,方便測試:
app.Run(async context => { await context.Response.WriteHtmlAsync(async res => { await res.WriteAsync($"<h2>Hello Cookie Authentication</h2>"); await res.WriteAsync("<a class=\"btn btn-default\" href=\"/profile\">個人信息</a>"); }); });
在瀏覽器中打開http://localhost:5000/,顯示 "Hello Cookie Authentication",點擊 「個人信息」 按鈕:
請求: GET http://localhost:5000/profile HTTP/1.1 Host: localhost:5000 響應: HTTP/1.1 302 Found Location: http://localhost:5000/Account/Login?ReturnUrl=%2Fprofile
由於咱們沒有登陸,會在受權中間件中會執行context.ChallengeAsync();
方法,最終會跳轉到登陸頁面,而後輸入用戶名密碼(alice/alice),登陸成功:
請求: POST http://localhost:5000/Account/Login?ReturnUrl=%2Fprofile HTTP/1.1 Host: localhost:5000 Content-Type: application/x-www-form-urlencoded returnUrl=%2Fprofile&userName=alice&password=alice 響應: HTTP/1.1 302 Found Location: /profile Set-Cookie: .AspNetCore.Cookies=CfDJ8B4XRZETkRhMt3mT9VduB8KTzFhbcuszdNDXZpaQI3zSOcOD8uAzjr-iHzNCPVgXrKqxfK-MrP5d5r9X1zfKOgg2_j54t0ccAQ5nshSmXnRvjIZ6id3GD5fDP9v2x1iV0JE7X9IdoA458DZjx6qm6971GeY5HYVnT7odwgQR8eRaHo0-Wacmt95QuC9IVSapqsShHOeu5ZowFmDAPXrlUHOSwBPAjiLkf8mNbu8U4ZcWFlaBXC9-H-2_ts5wyi-90zw6jGxX3o7tRiQB4qq8IDmIJbZtN4Nl8TKHHcTbyFl5Z__MrgrjJ7s4cGdnIoDJWB9ENw1IGRgF3Rib8KmhkwhlUyO2VMnuVI8vSP2PcwrkUGtudJwHMHrA8cuS021xpmIhkhgW3e82r_0_jxAh1nqG4zwTP5i8iLU6FsOLLWatveSWB441Ntqw-L-pYczsBAYFRT0Hh56ofUAxGd7aaGtDx0jvuuxW5gK245Pf0TKG-4G46yDwLrFtjNcN_GREbpwtHAz-I7XqiDZgS3nbzjik5s05NxB7d6X3aOFc5JHCwFxW-i-xW-ToJLZrp3Jo8W0bAxVwxZIW2PwZlVtyeYSkqByFRaDS4qcBywE2Bmar_TyJm9UpVWaL2s9KxpU_DHN6meYne5E5dH4-k1DoABl6FyNPn6xYfMWxzu0_7ZFhVJjCycScy1jggCv4Hk5nkltj9A3QrFpNb_HCk21Uek9g-7Zi150EKfDzhGjMto5_hbWcmQtUsHuLbZlnYTHXZ-7zELZOepAUts2ZGoUnEaI; path=/; samesite=lax; httponly
能夠看到,響應報文在Cookie中附加了身份令牌,並會跳轉到以前未登陸時訪問的頁面/profile
,跳轉後顯示以下:
如上,由於瀏覽器會自動附帶上剛纔寫入的Cookie,因此受權經過,並展現出咱們在登陸時設置的Claim。
最後,點擊 「退出」 ,響應報文中會將Cookie設置爲空(清除Cookie):
請求: GET http://localhost:5000/Account/Logout HTTP/1.1 Host: localhost:5000 響應: HTTP/1.1 302 Found Location: / Set-Cookie: .AspNetCore.Cookies=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; samesite=lax
在上面的演示中,咱們能夠看到Cookie中的值很是的長,而咱們設置的Claim並很少,這是由於微軟內置的ClaimTypes
都是一大串的ULR地址。而對於 ASP.NET Core 自己來講,它並不關心你使用的ClaimType是什麼,只要你讀取與保存時使用的ClaimType保持一致就沒有問題。咱們可使用簡短的字符串name
來代替ClaimTypes.Name
,可是推薦的作法是直接使用JwtClaimTypes
,由於它夠簡短並且通用。
首先添加ClaimTypes
的Package引用:
dotnet add package IdentityModel --version 2.12.0
而後,將以前的添加Claim的代碼修改以下:
var claimIdentity = new ClaimsIdentity("Cookie", JwtClaimTypes.Name, JwtClaimTypes.Role); claimIdentity.AddClaim(new Claim(JwtClaimTypes.Id, user.Id.ToString())); claimIdentity.AddClaim(new Claim(JwtClaimTypes.Name, user.Name)); claimIdentity.AddClaim(new Claim(JwtClaimTypes.Email, user.Email)); claimIdentity.AddClaim(new Claim(JwtClaimTypes.PhoneNumber, user.PhoneNumber)); claimIdentity.AddClaim(new Claim(JwtClaimTypes.BirthDate, user.Birthday.ToString()));
須要注意的是在建立ClaimsIdentity
時須要手動指定它的NameType和RoleType,不然它將會使用默認的ClaimTypes.Name和ClaimTypes.Role,這樣會致使咱們從ClaimsPrincipal中獲取Identity.Name
屬性和執行IsInRole
檢查時失敗。
運行,從新登陸,看看效果如何:
請求: POST http://localhost:5000/Account/Login?ReturnUrl=%2Fprofile HTTP/1.1 Host: localhost:5000 Content-Type: application/x-www-form-urlencoded returnUrl=%2Fprofile&userName=alice&password=alice 響應: HTTP/1.1 302 Found Date: Sun, 24 Sep 2017 06:04:28 GMT Location: /profile Set-Cookie: .AspNetCore.Cookies=CfDJ8B4XRZETkRhMt3mT9VduB8JV9NE2zJ9YVQmTpAU3E9Op9rQHvJ7WvdcrarbTGWE7c_e2aLpoZCdDJ7-0fTFZGUwuLVMC0vD_eeE9ct2Vj7gHCPCVeK3qQPsQ2lNmKvPwPf82-CURFXGgFC1y-N17tXdT7RoZhLHskIHx7qNcxeicS7wiSDhQD3l3mgOgq0bdjWJTk3LnpHk8zS0fDhKp6Vd6vFvCyzzRJu1ax5Y27Bg3dZp4Zsa3I9HAp5wXmyp51de8scS25nyaV0FEd1YUWgC1LsuwOODrSPqMkokv7XQXQc8W212O2dHbuuJ1xYEr1i5_Gl1syIX3ZuPj1_wqcnAKu5keY0ZVJz45iGYIRC09hd4n8j1SEA8dDlhbslCtyZ6xMt6MdRFv1D7fhbt_g4RGDk7ZkjpnT6z9q3dTWNzkS3gSd9AekBNbUNw9ojZmTWoCFhZgxz-6Wwtcp9z7vIo; path=/; samesite=lax; httponly
是否是短了不少?好吧,其實感受仍是有點長,不要緊,下面再介紹一種更加完全的優化方式。
而最終頁面上展現的Claims信息以下:
終極的解決方案就是參考Session的原理,把Claims信息則保存在服務端,併爲其設置一個ID,Cookie中則只保存該ID,這樣就能夠在服務端經過該ID來檢索出完整的Claims信息。不過注意,這並非在使用 ASP.NET Core 中的Session,只是參考其存儲方式。
那麼怎麼作呢?在前面註冊Cookie認證時,使用的AddCookie
方法中,其CookieAuthenticationOptions
參數還能夠設置一個ITicketStore
類型的SessionStore
屬性,咱們能夠經過實現該接口來自定義Cookie的存取方式,在這裏,使用本地緩存來實現:
首先添加Microsoft.Extensions.Caching.Memory
的Package引用:
dotnet add package Microsoft.Extensions.Caching.Memory --version 2.0.0
而後,定義MemoryCacheTicketStore類:
public class MemoryCacheTicketStore : ITicketStore { private const string KeyPrefix = "CSS-"; private IMemoryCache _cache; public MemoryCacheTicketStore() { _cache = new MemoryCache(new MemoryCacheOptions()); } public async Task<string> StoreAsync(AuthenticationTicket ticket) { var key = KeyPrefix + Guid.NewGuid().ToString("N"); await RenewAsync(key, ticket); return key; } public Task RenewAsync(string key, AuthenticationTicket ticket) { var options = new MemoryCacheEntryOptions(); var expiresUtc = ticket.Properties.ExpiresUtc; if (expiresUtc.HasValue) { options.SetAbsoluteExpiration(expiresUtc.Value); } options.SetSlidingExpiration(TimeSpan.FromHours(1)); _cache.Set(key, ticket, options); return Task.FromResult(0); } public Task<AuthenticationTicket> RetrieveAsync(string key) { _cache.TryGetValue(key, out AuthenticationTicket ticket); return Task.FromResult(ticket); } public Task RemoveAsync(string key) { _cache.Remove(key); return Task.FromResult(0); } }
將MemoryCacheTicketStore
配置到CookieAuthenticationOptions中:
.AddCookie(options =>
{
options.SessionStore = new MemoryCacheTicketStore(); });
再次從新登陸,響應以下:
HTTP/1.1 302 Found Location: /profile Set-Cookie: .AspNetCore.Cookies=CfDJ8B4XRZETkRhMt3mT9VduB8JOI85seEY347RswRzSiL_BQlJTb4JeFqpJzXNW8xOH1CwUKjwsx4CJWyMV5Wwq61IV0Kz4If0LmmJpEicZi2uxmyE2jcCXw_IRaPOaP0eJYM-DkpsjlA_Qu9knFxrpGQaI_BuRbUbbVhy62V5vjwMzoSewmQiPblS1PbPiqXfjAGmF_ZaSM40kwNOboAP_SMoJjX0AtEzmsUqECWFPZLxLoOJJ10Kz16cnSjtxha_KXY7i8f95jVbnX3cj79-GQ5iXnRePBBR_2LsXI5eDW_6E; path=/; samesite=lax; httponly
這樣,Cookie中的值就很是簡短了(因爲其還包含AuthenticationProperties序列化後的值,並無想象中的短),而且Cookie中的值不會再隨着咱們設置的Claims的增長而變長,在分佈式環境下則可使用分佈式緩存來保存。
對於認證系統,身份令牌都會有一個有效期的概念,而Cookie認證中默認有效期是14天,所以只要瀏覽器沒有清除Cookie,而且Cookie沒有過時,便麼就一直能夠驗證經過。可是,若是用戶修改了密碼,咱們但願該Cookie失效,或者是用戶更新了Claims的信息時,咱們但願從新生成Cookie,不然咱們取到的仍是舊的Claims信息。那麼,該怎麼作呢?
對此,網上比較流行的作法是在用戶數據庫中添加一個安全字段,當用戶修改了一些安全性信息時,便更新該字段,並在Claim中加入此字段,一塊兒寫入到Cookie中,驗證時即可以判斷該字段是否與數據庫一致,若不一致則驗證失敗或從新生成:
public static class LastChangedValidator { public static async Task ValidateAsync(CookieValidatePrincipalContext context) { var userRepository = context.HttpContext.RequestServices.GetRequiredService<IUserRepository>(); var userPrincipal = context.Principal; string lastChanged = (from c in userPrincipal.Claims where c.Type == "LastUpdated" select c.Value).FirstOrDefault(); if (string.IsNullOrEmpty(lastChanged) || !userRepository.ValidateLastChanged(userPrincipal, lastChanged)) { // 1. 驗證失敗 等同於 Principal = principal; context.RejectPrincipal(); // 2. 驗證經過,並會從新生成Cookie。 context.ShouldRenew = true; } } }
如上,1 和 2 兩種方式,咱們能夠根據實際狀況選擇一種,而不該該同時存在。
在Cookie認證的配置中,提供了一系列的事件,其中便有一個OnValidatePrincipal
事件,用來附加服務端的驗證邏輯:
.AddCookie(options =>
{
options.Events = new CookieAuthenticationEvents { OnValidatePrincipal = LastChangedValidator.ValidateAsync }; });
如上,便完成了該事件的註冊,不過該驗證一般會查詢數據庫,損耗較大,能夠經過設置驗證週期來提升性能,如:每5分鐘執行驗證一次(在MVC5中是有該配置的,Core中暫未發現)。
對於Cookie來講,默認的過時時間爲Session,即關閉瀏覽器後就清除。一般在用戶登陸時會提供一個記住個人選項,用來保證在關閉瀏覽時不清除Cookie。而在SignInAsync
方法中,還接收一個AuthenticationProperties
類型的參數,能夠用來指定Cookie是否持久化以及過時時間:
await HttpContext.SignInAsync("MyCookieAuthenticationScheme", principal, new AuthenticationProperties { // 持久保存 IsPersistent = true // 指定過時時間 ExpiresUtc = DateTime.UtcNow.AddMinutes(20) });
看一下CookieAuthenticationHandler中SignInAsync
方法關於該配置的實現:
if (!signInContext.Properties.ExpiresUtc.HasValue) { signInContext.Properties.ExpiresUtc = issuedUtc.Add(Options.ExpireTimeSpan); } if (signInContext.Properties.IsPersistent) { var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan); signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime(); }
只有在IsPersistent
爲True時,纔會在寫入Cookie指定Expires
。須要注意的是瀏覽器中的Cookie過時時間僅僅是用來指定瀏覽器是否刪除Cookie,而在Cookie存儲的值中,也會包含該Cookie認證的發佈時間和過時時間等,並在HandleAuthenticateAsync
方法中對會其進行驗證,並非說只要你有Cookie就能驗證經過。
AddCookie已屢次用過,無需多說,直接看源碼:
public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder) => builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, null, null); public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<CookieAuthenticationOptions> configureOptions) { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>()); return builder.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>(authenticationScheme, displayName, configureOptions); }
其實現很是簡單,首先註冊了Cookie認證的配置項CookieAuthenticationOptions
,而authenticationScheme
參數用來指定當前認證的惟一的標識,不能重複。一般,使用默認的CookieAuthenticationDefaults.AuthenticationScheme
就能夠了,可是當咱們同時使用多個Cookie認證方式時,須要手動爲他們指定不一樣的Scheme。
最後,直接調用上一章中介紹的AddScheme,完成對CookieAuthenticationHandler
的註冊。
CookieAuthenticationOptions是針對Cookie認證的各類配置,如重定向地址,認證階段事件的註冊,Cookie名,過時時間等等,首先看一下它的定義:
public class CookieAuthenticationOptions : AuthenticationSchemeOptions { private CookieBuilder _cookieBuilder = new RequestPathBaseCookieBuilder { SameSite = SameSiteMode.Lax, HttpOnly = true, SecurePolicy = CookieSecurePolicy.SameAsRequest, }; public CookieAuthenticationOptions() { ExpireTimeSpan = TimeSpan.FromDays(14); ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter; SlidingExpiration = true; Events = new CookieAuthenticationEvents(); } public CookieBuilder Cookie { get => _cookieBuilder; set => _cookieBuilder = value ?? throw new ArgumentNullException(nameof(value)); } public new CookieAuthenticationEvents Events { get => (CookieAuthenticationEvents)base.Events; set => base.Events = value; } public ITicketStore SessionStore { get; set; } // 當用戶未登陸時,重定向到該路徑,默認:/Account/Login public PathString LoginPath { get; set; } // 指定登出的路徑,默認:/Account/Logout public PathString LogoutPath { get; set; } // 當用戶無權訪問時,重定向到該路徑,默認:/Account/AccessDenied public PathString AccessDeniedPath { get; set; } // 返回地址參數名,默認:ReturnUrl public string ReturnUrlParameter { get; set; } // 指定Cookie的過時時間 public TimeSpan ExpireTimeSpan { get; set; } // 當Cookie過時時間已達一半時,是否重置爲ExpireTimeSpan public bool SlidingExpiration { get; set; } // 用來將Cookie寫入到瀏覽器或刪除 public ICookieManager CookieManager { get; set; } public IDataProtectionProvider DataProtectionProvider { get; set; } public ISecureDataFormat<AuthenticationTicket> TicketDataFormat { get; set; } }
在 ASP.NET Core 2.0 中對針對Cookie的配置集中放到CookieBuilder
類型當中,相比以前更加清晰:
public class CookieBuilder : object { public virtual string Name { get; set; } public virtual string Path { get; set; } public virtual string Domain { get; set; } public virtual bool HttpOnly { get; set; } public virtual SameSiteMode SameSite { get; set; } public virtual CookieSecurePolicy SecurePolicy { get; set; } public virtual TimeSpan? Expiration { get; set; } public virtual TimeSpan? MaxAge { get; set; } public CookieOptions Build(HttpContext context); }
都是一些針對Cookie配置的標準用法,無需多說。
CookieAuthenticationEvents爲咱們提供了在Cookie認證的各個階段(如,登陸先後,退出先後,重定向等)註冊事件的機會,以便咱們攔截一些默認行爲,來自定義處理邏輯。
public class CookieAuthenticationEvents { public virtual Task ValidatePrincipal(CookieValidatePrincipalContext context) => OnValidatePrincipal(context); public virtual Task SigningIn(CookieSigningInContext context) => OnSigningIn(context); public virtual Task SignedIn(CookieSignedInContext context) => OnSignedIn(context); public virtual Task SigningOut(CookieSigningOutContext context) => OnSigningOut(context); public virtual Task RedirectToLogout(RedirectContext<CookieAuthenticationOptions> context) => OnRedirectToLogout(context); public virtual Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context) => OnRedirectToLogin(context); public virtual Task RedirectToReturnUrl(RedirectContext<CookieAuthenticationOptions> context) => OnRedirectToReturnUrl(context); public virtual Task RedirectToAccessDenied(RedirectContext<CookieAuthenticationOptions> context) => OnRedirectToAccessDenied(context); }
每個事件都有它的默認實現,這裏就再也不多說,咱們能夠根據實際狀況進行註冊。
CookieAuthenticationHandler即是Cookie認證的具體實現:
public class CookieAuthenticationHandler : AuthenticationHandler<CookieAuthenticationOptions>, IAuthenticationSignInHandler, IAuthenticationSignOutHandler { ... protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { var result = await EnsureCookieTicket(); if (!result.Succeeded) { return result; } var context = new CookieValidatePrincipalContext(Context, Scheme, Options, result.Ticket); // 執行前而介紹的服務端驗證 await Events.ValidatePrincipal(context); if (context.ShouldRenew) { // 從新生成Cookie RequestRefresh(result.Ticket); } return AuthenticateResult.Success(new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name)); } public async virtual Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) { ... var ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.Scheme.Name); .... var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding()); Options.CookieManager.AppendResponseCookie(Context, Options.Cookie.Name, cookieValue, signInContext.CookieOptions); var signedInContext = new CookieSignedInContext(Context, Scheme, signInContext.Principal, signInContext.Properties, Options); await Events.SignedIn(signedInContext); var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath; await ApplyHeaders(shouldRedirect, signedInContext.Properties); Logger.SignedIn(Scheme.Name); } }
其核心方法HandleAuthenticateAsync
會檢查請求Cookie,查找與CookieBuilder.Name
對應的Cookie值,解密反序列化成AuthenticationTicket
對象,最後在上一章介紹的AuthenticationMiddleware中間件中將Principal
賦予給HttpContext。
而CookieAuthenticationHandler還實現了IAuthenticationSignInHandler
和IAuthenticationSignOutHandler
,這也是ASP.NET Core中內置的惟一支持登陸和退出的認證方式。在SignInAsync
方法中使用ClaimsPrincipal來建立一個AuthenticationTicket
對象,而後將其加密,寫入到Cookie中,便完成了登陸(身份令牌的發放),而SignOutAsync
方法則只是簡單的刪除Cookie。
篇幅有限,就再也不多說,感興趣的能夠去看一下完整代碼:CookieAuthenticationHandler。
Cookie認證是一種本地認證方式,也是最爲簡單,最爲經常使用的認證方式。其認證邏輯也很簡單,總結一下就是獲取請求中指定的Cookie,解密成功後,反序列生成 AuthenticationTicket 對象,並進行一系列的驗證,而登陸方法與之對應:根據用戶信息建立 AuthenticationTicket 對象,並加密後序列化,寫入到Cookie中。在下一章中,就來介紹一下最爲流行的遠程認證方式:OAuth 和 OpenID Connect。
最後附上本文中的示例代碼:https://github.com/RainingNight/AspNetCoreSample/tree/master/src/Functional/Authentication/CookieSample。