注:本文隸屬於《理解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 = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name";
public const string NameIdentifier = "http://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