理解ASP.NET Core – 基於Cookie的身份認證(Authentication)
- 2022 年 1 月 17 日
- 筆記
- .NET, Asp.Net Core, cookie, 身份認證
註:本文隸屬於《理解ASP.NET Core》系列文章,請查看置頂博客或點擊此處查看全文目錄
概述
通常,身份認證(Authentication)和授權(Authorization)都會放在一起來講。但是,由於這倆英文相似,且「認證授權」四個字經常連着用,導致一些剛接觸這塊知識的讀者產生混淆,分不清認證和授權的區別,甚至認為這倆是同一個。所以,我想先給大家簡單區分一下身份認證和授權。
身份認證
確認執行操作的人是誰。
當用戶請求後台服務時,系統首先需要知道用戶是誰,是張三、李四還是匿名?確認身份的這個過程就是「身份認證」。在我們的實際生活中,通過出示自己的身份證,別人就可以快速地確認你的身份。
授權
確認操作人是否有執行該項操作的權限。
確認身份後,已經獲悉了用戶信息,隨後來到授權階段。在本階段,要做的是確認用戶有沒有執行該項操作的權限,如確認張三有沒有商品查看權限、有沒有編輯權限等。
Cookie
Cookie
對於許多人來說,是一個再熟悉不過的東西,熟悉到現在的Web應用,基本離不開它,如果你對Cookie還不太了解,也別慌,我在文末給大家整理了一些高質量的文章,推薦對Cookie有一個整體的了解之後,再來繼續閱讀下方的內容!
基於Cookie進行身份認證,通常的方案是用戶成功登錄後,服務端將用戶的必要信息記錄在Cookie中,並發送給瀏覽器,後續當用戶發送請求時,瀏覽器將Cookie傳回服務端,服務端就可以通過Cookie中的信息確認用戶信息了。
在開始之前,為了方便大家理解並能夠實際操作,我已經準備好了一個示例程序,請訪問XXTk.Auth.Samples.Cookies.Web獲取源碼。文章中的代碼,基本上在示例程序中均有實現,強烈建議組合食用!
身份認證(Authentication)
添加身份認證中間件
在 ASP.NET Core 中,為了進行身份認證,需要在HTTP請求管道中通過UseAuthentication
添加身份認證中間件——AuthenticationMiddleware
:
public class Startup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
// 身份認證中間件
app.UseAuthentication();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
UseAuthentication
一定要放在UseEndpoints
之前,否則Controller
中無法通過HttpContext
獲取身份信息。
AuthenticationMiddleware
做的事情很簡單,就是確認用戶身份,在代碼層面上就是給HttpContext.User
賦值,請參考下方代碼:
public class AuthenticationMiddleware
{
private readonly RequestDelegate _next;
public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
{
_next = next;
Schemes = schemes;
}
public IAuthenticationSchemeProvider Schemes { get; set; }
public async Task Invoke(HttpContext context)
{
// 記錄原始路徑和原始基路徑
context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
{
OriginalPath = context.Request.Path,
OriginalPathBase = context.Request.PathBase
});
// 如果有顯式指定的身份認證方案,優先處理(這裡不用看,直接看下面)
var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
{
var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
if (handler != null && await handler.HandleRequestAsync())
{
return;
}
}
// 使用默認的身份認證方案進行認證,並賦值 HttpContext.User
var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
if (defaultAuthenticate != null)
{
var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
if (result?.Principal != null)
{
context.User = result.Principal;
}
}
await _next(context);
}
}
配置Cookie認證方案
現在,認證中間件已經加好了,現在需要在ConfigureServices
方法中添加身份認證所需要用到的服務並進行認證方案配置。
我們可以通過AddAuthentication
擴展方法來添加身份認證所需要的服務,並可選的指定默認認證方案的名稱,以下方為例:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
我們添加了身份認證所依賴的服務,並指定了一個名為CookieAuthenticationDefaults.AuthenticationScheme
的默認認證方案,即Cookies
。很明顯,它是一個基於Cookie的身份認證方案。
CookieAuthenticationDefaults
是一個靜態類,定義了一些常用的默認值:
public static class CookieAuthenticationDefaults
{
// 認證方案名
public const string AuthenticationScheme = "Cookies";
// Cookie名字的前綴
public static readonly string CookiePrefix = ".AspNetCore.";
// 登錄路徑
public static readonly PathString LoginPath = new PathString("/Account/Login");
// 註銷路徑
public static readonly PathString LogoutPath = new PathString("/Account/Logout");
// 訪問拒絕路徑
public static readonly PathString AccessDeniedPath = new PathString("/Account/AccessDenied");
// return url 的參數名
public static readonly string ReturnUrlParameter = "ReturnUrl";
}
現在,我們已經指定了默認認證方案,接下來就是來配置這個方案的細節,通過後跟AddCookie
來實現:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
// 在這裡對該方案進行詳細配置
});
}
}
很明顯,AddCookie
的第一個參數就是指定該認證方案的名稱,第二個參數是詳細配置。
通過options
,可以針對登錄、註銷、Cookie等方面進行詳細配置。它的類型為CookieAuthenticationOptions
,繼承自AuthenticationSchemeOptions
。 屬性實在比較多,我就選擇一些比較常用的來講解一下。
另外,由於在針對選項進行配置時,需要依賴DI容器中的服務,所以不得不將選項的配置從AddCookie
擴展方法中提出來。
請查看以下代碼:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme)
.Configure<IDataProtectionProvider>((options, dp) =>
{
options.LoginPath = new PathString("/Account/Login");
options.LogoutPath = new PathString("/Account/Logout");
options.AccessDeniedPath = new PathString("/Account/AccessDenied");
options.ReturnUrlParameter = "returnUrl";
options.ExpireTimeSpan = TimeSpan.FromDays(14);
//options.Cookie.Expiration = TimeSpan.FromMinutes(30);
//options.Cookie.MaxAge = TimeSpan.FromDays(14);
options.SlidingExpiration = true;
options.Cookie.Name = "auth";
//options.Cookie.Domain = ".xxx.cn";
options.Cookie.Path = "/";
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.IsEssential = true;
options.CookieManager = new ChunkingCookieManager();
options.DataProtectionProvider ??= dp;
var dataProtector = options.DataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", CookieAuthenticationDefaults.AuthenticationScheme, "v2");
options.TicketDataFormat = new TicketDataFormat(dataProtector);
options.Events.OnSigningIn = context =>
{
Console.WriteLine($"{context.Principal.Identity.Name} 正在登錄...");
return Task.CompletedTask;
};
options.Events.OnSignedIn = context =>
{
Console.WriteLine($"{context.Principal.Identity.Name} 已登錄");
return Task.CompletedTask;
};
options.Events.OnSigningOut = context =>
{
Console.WriteLine($"{context.HttpContext.User.Identity.Name} 註銷");
return Task.CompletedTask;
};
options.Events.OnValidatePrincipal += context =>
{
Console.WriteLine($"{context.Principal.Identity.Name} 驗證 Principal");
return Task.CompletedTask;
};
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
以上配置,大多使用了程序的默認值,接下來一一進行詳細講解:
LoginPath
:登錄頁路徑,指向一個Action
。- 默認
/Account/Login
。 - 當服務端不允許匿名訪問而需要確認用戶信息時,跳轉到該頁面進行登錄。
- 另外,登錄方法通常會有一個參數,叫作
return url
,用來當用戶登錄成功時,自動跳轉回之前訪問的頁面。這個參數也會自動傳遞給該Action
,下方會詳細說明。
- 默認
LogoutPath
:註銷路徑,指向一個Action
。默認/Account/Logout
。AccessDeniedPath
:訪問拒絕頁路徑,指向一個Action
。默認/Account/AccessDenied
。當出現Http狀態碼 403 時,會跳轉到該頁面。ReturnUrlParameter
:上面提到的return url
的參數名,參數值會通過 query 的方式傳遞到該參數中。默認ReturnUrl
。ExpireTimeSpan
:認證票據(authentication ticket)的有效期。- 默認 14 天
- 認證票據在代碼中表現為類型為
AuthenticationTicket
的對象,它就好像一個手提包,裏面放滿了可以證明你身份的物品,如身份證、駕駛證等。 - 認證票據存儲在Cookie中,它的有效期與所在Cookie的有效期是獨立的,如果Cookie沒有過期,但是認證票據過期了,也無法通過認證。在下方講解登錄部分時,有針對認證票據有效期的詳細說明。
Cookie.Expiration
:Cookie的過期時間,即在瀏覽器中的保存時間,用於持久化Cookie。- 對應Cookie中的
Expires
屬性,是一個明確地時間點。 - 目前已被禁用,我們無法給它賦值。
- 對應Cookie中的
Cookie.MaxAge
:Cookie的過期時間,即在瀏覽器中的保存時間,用於持久化Cookie。- 對應Cookie中的
Max-Age
屬性,是一個時間範圍。 - 如果Cookie的
Max-Age
和Expires
同時設置,則以Max-Age
為準 - 如果沒有設置Cookie的
Expires
,同時Cookie.MaxAge
的值保持為null
,那麼該Cookie的有效期就是當前會話(Session),當瀏覽器關閉後,Cookie便會被清除(實際上,現在的部分瀏覽器有會話恢復功能,瀏覽器關閉後重新打開,Cookie也會跟着恢復,彷彿瀏覽器從未關閉一樣)。
- 對應Cookie中的
SlidingExpiration
:指示Cookie的過期方式是否為滑動過期。默認true
。若為滑動過期,服務端收到請求後,如果發現Cookie的生存期已經超過了一半,那麼服務端會重新頒發一個全新的Cookie,Cookie的過期時間和認證票據的過期時間都會被重置。Cookie.Name
:該Cookie的名字,默認是.AspNetCore.Cookies
。Cookie.Domain
:該Cookie所屬的域,對應Cookie的Domain
屬性。一般以「.」開頭,允許subdomain都可以訪問。默認為請求Url的域。Cookie.Path
:該Cookie所屬的路徑,對應Cookie的Path
屬性。默認/
。Cookie.SameSite
:設置通過瀏覽器跨站發送請求時決定是否攜帶Cookie的模式,共有三種,分別是None
、Lax
和Strict
。public enum SameSiteMode { Unspecified = -1, None, Lax, Strict }
SameSiteMode.Unspecified
:使用瀏覽器的默認模式。SameSiteMode.None
:不作限制,通過瀏覽器發送同站或跨站請求時,都會攜帶Cookie。這是非常不建議的模式,容易受到CSRF攻擊
SameSiteMode.Lax
:默認值。通過瀏覽器發送同站請求或跨站的部分GET請求時,可以攜帶Cookie。SameSiteMode.Strict
:只有通過瀏覽器發送同站請求時,才會攜帶Cookie。- 更具體的內容,參考最下方的好文推薦
Cookie.HttpOnly
:指示該Cookie能否被客戶端腳本(如js)訪問。默認為true
,即禁止客戶端腳本訪問,這可以有效防止XSS攻擊
。Cookie.SecurePolicy
:設置Cookie的安全策略,對應於Cookie的Secure
屬性。public enum CookieSecurePolicy { SameAsRequest, Always, None }
CookieSecurePolicy.Always
:設置Secure=true
,當發送登錄請求和後續請求均為Https時,瀏覽器才將Cookie發送給服務端。CookieSecurePolicy.None
:不設置Secure
,即發送Http請求和Https請求時,瀏覽器都會將Cookie發送給服務端。CookieSecurePolicy.SameAsRequest
:默認值。視情況而定,如果登錄接口是Https請求,則設置Secure=true
,否則,不設置。
Cookie.IsEssential
:指示該Cookie對於應用的正常運行是必要的,不需要經過用戶同意使用CookieManager
:Cookie管理器,用於添加響應Cookie、查詢請求Cookie或刪除Cookie。默認是ChunkingCookieManager
。DataProtectionProvider
:認證票據加密解密提供器,可以按需提供相應的加密解密工具。默認是KeyRingBasedDataProtector
。有關數據保護相關的知識,請參考官方文檔-ASP.NET Core數據保護。TicketDataFormat
:認證票據的數據格式,內部通過DataProtectionProvider
提供的加密解密工具進行認證票據的加密和解密。默認是TicketDataFormat
。
以下是部分事件回調:
Events.OnSigningIn
:登錄前回調Events.OnSignedIn
:登錄後回調Events.OnSigningOut
:註銷時回調Events.OnValidatePrincipal
:驗證 Principal 時回調
如果你覺得這樣註冊回調不優雅,那你可以繼承自CookieAuthenticationEvents
來實現自己的類,內部重寫對應的方法即可,如:
public class MyCookieAuthenticationEvents : CookieAuthenticationEvents {}
最後,在options
處進行替換即可:options.EventsType = typeof(MyCookieAuthenticationEvents);
- 跨域(Cross Origin):請求的Url與當前頁面的Url進行對比,協議、域名、端口號中任意一個不同,則視為跨域。
- 跨站(Cross Site):跨站相對於跨域來說,規則寬鬆一些,請求的Url與當前頁面的Url進行對比,eTLD + 1不同,則視為跨站。
用戶登錄和註銷
用戶登錄
現在,終於到了用戶登錄和註銷了。還記得嗎,方案中配置的登錄、註銷、禁止訪問路徑要和接口對應起來。
ASP.NET Core針對登錄,提供了HttpContext
的擴展方法SignInAsync
,我們可以使用它進行登錄。以下僅貼出Controller的代碼,前端代碼請參考github的源碼。
public class AccountController : Controller
{
[HttpGet]
public IActionResult Login([FromQuery] string returnUrl = null)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
[HttpPost]
public async Task<IActionResult> Login([FromForm] LoginViewModel input)
{
ViewBag.ReturnUrl = input.ReturnUrl;
// 用戶名密碼相同視為登錄成功
if (input.UserName != input.Password)
{
ModelState.AddModelError("UserNameOrPasswordError", "無效的用戶名或密碼");
}
if (!ModelState.IsValid)
{
return View();
}
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
identity.AddClaims(new[]
{
new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString("N")),
new Claim(ClaimTypes.Name, input.UserName)
});
var principal = new ClaimsPrincipal(identity);
// 登錄
var properties = new AuthenticationProperties
{
IsPersistent = input.RememberMe,
ExpiresUtc = DateTimeOffset.UtcNow.AddSeconds(60),
AllowRefresh = true
};
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, properties);
if (Url.IsLocalUrl(input.ReturnUrl))
{
return Redirect(input.ReturnUrl);
}
return Redirect("/");
}
}
首先說一下Claim
、Identity
和Principal
:
Claim
:表示一條信息的聲明。以我們的身份證為例,裏面包含姓名、性別等信息,如「姓名:張三」、「性別:男」,這些都是Claim。Identity
:表示一個身份。對於一個ClaimsIdentity
來說,它是由一個或多個Claim組成的。我們的身份證就是一個Identity。Principal
:表示用戶本人。對於一個ClaimsPrincipal
來說,它是由一個或多個ClaimsIdentity組成的。想一下,我們每個人的身份不僅僅只有一種,除了身份證外,還有駕駛證、會員卡等。
回到Login
方法,首先聲明了一個ClaimsIdentity
實例,並將CookieAuthenticationDefaults.AuthenticationScheme
作為認證類型來傳入。需要注意的是,這個認證類型一定不要是null
或空字符串,否則,默認配置下,你會得到如下錯誤:
InvalidOperationException: SignInAsync when principal.Identity.IsAuthenticated is false is not allowed when AuthenticationOptions.RequireAuthenticatedSignIn is true.
隨後,我們將用戶的一些非敏感信息作為Claim存入到了ClaimsIdentity中,並最終將其放入ClaimsPrincipal
實例。
在SignInAsync
擴展方法中,我們可以針對認證進行一些配置,通過AuthenticationProperties
。
IsPersistent
:票據是否持久化,即票據所在的Cookie是否持久化。如果持久化,則會將下方ExpiresUtc
的值設置為Cookie的Expires
屬性。默認為false
。ExpiresUtc
:票據的過期時間,默認為null
,如果為null
,則CookieAuthenticationHandler
會在HandleSignInAsync
方法中將Cookie認證方案配置中的CookieAuthenticationOptions.ExpireTimeSpan
+AuthenticationProperties.IssuedUtc
的結果賦值給該屬性。AllowRefresh
:上面提到過,在Cookie的認證方案配置中,可以將過期方式配置為滑動過期,滿足條件時,會重新頒發Cookie。實際上,要實現這個效果,還要將AllowRefresh
設置為null
或者true
才可以。默認為null
。IssuedUtc
:票據頒發時間,默認為null
。一般無需手動賦值,為null
時,CookieAuthenticationHandler
會在HandleSignInAsync
方法中將當前時間賦值給該屬性。
這裡針對認證票據的有效期詳細說明一下:
通過上面我們已經得知,認證票據的有效期是通過AuthenticationProperties.ExpiresUtc
來設置的,它是一個明確的時間點,如果我們沒有手動賦值給該屬性,那麼Cookie的認證處理器CookieAuthenticationHandler
會將Cookie認證方案配置中的CookieAuthenticationOptions.ExpireTimeSpan
+ AuthenticationProperties.IssuedUtc
的結果賦值給該屬性。
而我們又知道,在配置Cookie認證方案時,Cookie.Expiration
屬性表示的是Cookie的Expires
屬性,但是它被禁用了,如果強行使用它,我們會得到這樣一段選項驗證錯誤信息:
Cookie.Expiration is ignored, use ExpireTimeSpan instead.
可是ExpireTimeSpan
屬性,注釋明確地說它指的不是Cookie的Expires
屬性,而是票據的有效期,這又是咋回事呢?其實,你可以想像一下以下場景:該Cookie的Expires
和Max-Age
都沒有被設置(程序允許它們為空),那麼該Cookie的有效期就是當前會話,但是,你通過設置AuthenticationProperties.IsPersistent = true
來表明該Cookie是持久化的,這就產生了歧義,實際上Cookie並沒有持久化,但是代碼卻認為它持久化了。所以,為了解決這個歧義,Cookie.Expiration
就被禁用了,而新增了一個ExpireTimeSpan
屬性,它除了可以作為票據的有效期外,還能在Cookie的Expires
和Max-Age
都沒有被設置但AuthenticationProperties.IsPersistent = true
的情況下,將值設置為Cookie的Expires
屬性,使得Cookie也被持久化。
我們看一下登錄效果:
-
未選擇「記住我」時:
-
選擇「記住我」時:
其他的特性自己摸索一下吧!
下面是SignInAsync 的核心內部細節模擬,更多細節請查看AuthenticationService
和CookieAuthenticationHandler
:
public class AccountController : Controller
{
private readonly IOptionsMonitor<CookieAuthenticationOptions> _cookieAuthOptionsMonitor;
public AccountController(IOptionsMonitor<CookieAuthenticationOptions> cookieAuthOptions)
{
_cookieAuthOptionsMonitor = cookieAuthOptions;
}
[HttpPost]
public async Task<IActionResult> Login([FromForm] LoginViewModel input)
{
// ...
var options = _cookieAuthOptionsMonitor.Get(CookieAuthenticationDefaults.AuthenticationScheme);
var ticket = new AuthenticationTicket(principal, properties, CookieAuthenticationDefaults.AuthenticationScheme);
// ticket加密
var cookieValue = options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding(HttpContext));
// CookieOptions 就隨便 new 個了,其實應該將 options 和 ticket 的配置轉化為 CookieOptions
options.CookieManager.AppendResponseCookie(HttpContext, options.Cookie.Name, cookieValue, new CookieOptions());
// ...
}
}
用戶註銷
註銷就比較簡單了,就是將Cookie清除,不再進行贅述:
[HttpPost]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync();
return Redirect("/");
}
可以看到名為「auth」的Cookie已被清空:
至此,一個簡單的基於Cookie的身份認證功能就實現了。
授權(Authorization)
添加授權中間件
要使用授權,需要先通過UseAuthorization
添加授權中間件——AuthorizationMiddleware
:
public class Startup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
// 身份認證中間件
app.UseAuthentication();
// 授權中間件
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
UseAuthorization
一定要放到UseRouting
和UseAuthentication
之後,因為授權中間件需要用到Endpoint
。另外,還要放到UseEndpoints
之前,否則請求在到達Controller之前,不會執行授權中間件。
授權配置
現在,授權中間件已經加好了,現在需要在ConfigureServices
方法中添加授權所需要用到的服務並進行額外配置。
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
options.InvokeHandlersAfterFailure = true;
});
}
}
DefaultPolicy
:默認的授權策略,默認為new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()
,即通過身份認證的用戶才能獲得授權。InvokeHandlersAfterFailure
:當存在多個授權處理器時,若其中一個失敗後,後續的處理器是否還繼續執行。默認為true
,即會繼續執行。
Url添加授權
現在,我們要求用戶登錄後才可以訪問/Home/Privacy
,為其添加特性[Authorize]
,不需要傳入策略policy
,就用默認策略即可:
public class HomeController : Controller
{
[HttpGet]
[Authorize]
public IActionResult Privacy()
{
return View();
}
}
你可以嘗試在其中訪問
HttpContext.User
,它其實就是我們登錄時創建的ClaimsPrincipal
。
全局Cookie策略
另外,我們可以通過UseCookiePolicy
針對Cookie策略進行全局配置。需要注意的是,CookiePolicyMiddleware
僅會對它之後添加的中間件起效,所以要盡量將它放在靠前的位置。
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Cookie全局策略
services.AddCookiePolicy(options =>
{
options.OnAppendCookie = context =>
{
Console.WriteLine("------------------ On Append Cookie --------------------");
Console.WriteLine($"Name: {context.CookieName}\tValue: {context.CookieValue}");
};
options.OnDeleteCookie = context =>
{
Console.WriteLine("------------------ On Delete Cookie --------------------");
Console.WriteLine($"Name: {context.CookieName}");
};
});
services.AddControllersWithViews();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
// Cookie 策略中間件
app.UseCookiePolicy();
// 身份認證中間件
app.UseAuthentication();
// 授權中間件
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
優化改進
優化Claim以減小身份認證Cookie體積
在用戶登錄時,驗證通過後,會添加Claims
,其中「類型」使用的是微軟提供的ClaimTypes
:
new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString("N")),
new Claim(ClaimTypes.Name, input.UserName)
細心地你會發現,ClaimTypes
的值太長了:
public static class ClaimTypes
{
public const string Name = "//schemas.xmlsoap.org/ws/2005/05/identity/claims/name";
public const string NameIdentifier = "//schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
}
我們可以使用JwtClaimTypes
進行優化:
public static class JwtClaimTypes
{
public const string Id = "id";
public const string Name = "name";
}
- 安裝 IdentityModel 包
Install-Package IdentityModel
- 進行替換,注意要在創建
ClaimsIdentity
實例時指定Name
和Role
的類型,這樣HttpContext.User.Identity.Name
和HttpContext.User.IsInRole(string role)
才能正常使用:
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme, JwtClaimTypes.Name, JwtClaimTypes.Role);
identity.AddClaims(new[]
{
new Claim(JwtClaimTypes.Id, Guid.NewGuid().ToString("N")),
new Claim(JwtClaimTypes.Name, input.UserName)
});
在服務端存儲Session信息
或許,你還是認為Cookie體積太大了,而且隨着Cookie中存儲信息的增加,還會越來越大,那你可以考慮將會話(Session)信息存儲在服務端進行解決,這也在一定程度上對數據安全作了保護。
這個方案非常簡單,我們將會話信息即認證票據保存在服務端而不是Cookie,Cookie中只需要存放一個SessionId。當請求發送到服務端時,會獲取到SessionId,通過它,就可以從服務端獲取到完整的Session信息。
會話信息的存儲介質多種多樣,可以是內存、也可以是分佈式存儲中間件,如Redis等,接下來我就以內存為例進行介紹(Redis的方案可以在我的示例程序源碼中找到,這裡就不貼了)。
在CookieAuthenticationOptions
中,有個SessionStore
,類型為ITicketStore
,用來定義會話的存儲,接下來我們就來實現它:
public class MemoryCacheTicketStore : ITicketStore
{
private const string KeyPrefix = "AuthSessionStore-";
private readonly IMemoryCache _cache;
private readonly TimeSpan _defaultExpireTimeSpan;
public MemoryCacheTicketStore(TimeSpan defaultExpireTimeSpan, MemoryCacheOptions options = null)
{
options ??= new MemoryCacheOptions();
_cache = new MemoryCache(options);
_defaultExpireTimeSpan = defaultExpireTimeSpan;
}
public async Task<string> StoreAsync(AuthenticationTicket ticket)
{
var guid = Guid.NewGuid();
var key = KeyPrefix + guid.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);
}
else
{
options.SetSlidingExpiration(_defaultExpireTimeSpan);
}
_cache.Set(key, ticket, options);
return Task.CompletedTask;
}
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.CompletedTask;
}
}
然後,只需要給CookieAuthenticationOptions.SessionStore
賦值就好了:
options.SessionStore = new MemoryCacheTicketStore(options.ExpireTimeSpan);
以下是一個存儲在Cookie中的SessionId示例,雖然還是很長,但是它並不會隨着信息量的增加而變大:
CfDJ8OGRqoEUgBZEu4m5Q8NfuATXjRKivKy7CR-oPpx2SaNJ8n1GWyBbPhNTEQzzIbZ62DqJPuxKtBJ752GqNxod9U5paaI_aQdH9EOH8nvgrinjvdHTneeKlhBvamEQrq7nA1e3wJOuQwFXRJASUphkS3kQzvc4-Upz27AAfoD510MC7YiwlhyxWl7agb8F0eeiilxAHDn4gskVqshu2hc5ENQAJNjXpa0yVaseryvsPrbukv5jqGC12WuUVe1cYhBIdWHHT61ZJcNtvNOAdtVlVA7i7RCJUBxNCUAhB-mw_s7R4GsNbU8aW7Ye9H-tx5067w
好文推薦
源碼請戳XXTk.Auth.Samples.Cookies.Web