ASP.NET Core Web Api之JWT VS Session VS Cookie(二)
前言
本文我們來探討下JWT VS Session的問題,這個問題本沒有過多的去思考,看到評論討論太激烈,就花了一點時間去研究和總結,順便說一句,這就是寫博客的好處,一篇博客寫出有的可能是經驗積累,有的可能是學習分享,但都逃不過看到文章的你有更多或更好的想法,往返交流自身能收穫更多,何樂而不爲呢?希望本文能解惑或者能得到更多的交流。我們可直接拋出問題:使用客戶端存儲的JWT比服務端維持Session更好嗎?
基於JWT和Session認證共同點
既然要比較JWT VS Session,那我們就得知道爲何需要JWT和Session,它們共同是爲了解決什麼問題呢?那我們從一個場景說起,網上購物現已是再平常不過的事情了,當我們將某個商品加入購物車後,然後跳轉到其他商品頁面此時需要之前選擇的商品依然在購物車中,此時就需要維持會話,因爲HTTP無狀態,所以JWT和Session共同點都是爲了持久維持會話而存在,爲了克服HTTP無狀態的情況,JWT和Session分別是如何處理的呢?
JWT VS Session認證
Session:當用戶在應用系統中登錄後,此時服務端會創建一個Session(我們也稱作爲會話),然後SessionId會保存到用戶的Cookie中,只要用戶是登錄狀態,對於每個請求,在Cookie中的SessionId都會發送到服務端,然後服務端會將保存在內存中的SessionId和Cookie中的SessionId進行比較來認證用戶的身份並響應。
JWT:當用戶在應用系統中登錄後,此時服務端會創建一個JWT,並將JWT發送到客戶端,客戶端存儲JWT(一般是在Local Storage中)同時在每個請求頭即Authorization中包含JWT,對於每個請求,服務端都會進行驗證JWT是否合法,直接在服務端本地進行驗證,比如頒發者,受理者等等,以致於無需發出網絡請求或與數據庫交互,這種方式可能比使用Session更快,從而加快響應性能,降低服務器和數據庫服務器負載。
通過如上對JWT認證和Session認證簡短的描述,我們知道二者最大的不同在於Session是存儲在服務端,而JWT存儲在客戶端。服務端存儲會話無外乎兩種,一種是將會話標識符存儲在數據庫,一種是存儲在內存中維持會話,我想大多數情況下都是基於內存來維持會話,但是這會帶來一定的問題,如果系統存在大流量,也就是說若有大量用戶訪問系統,此時使用基於內存維持的會話則限制了水平擴展,但對基於Token的認證則不存在這樣的問題,同時Cookie一般也只適用於單域或子域,如果對於跨域,假如是第三方Cookie,瀏覽器可能會禁用Cookie,所以也受瀏覽器限制,但對Token認證來說不是問題,因爲其保存在請求頭中。
如果我們將會話轉移到客戶端,也就是說使用Token認證,此時將解除會話對服務端的依賴,同時也可水平擴展,不受瀏覽器限制,但是與此同時也會帶來一定的問題,一是令牌的傳輸安全性,對於令牌傳輸安全性我們可使用HTTPS加密通道來解決,二是與存儲在Cookie中的SessionId相比,JWT顯然要大很多,因爲JWT中還包含用戶信息,所以爲了解決這個問題,我們儘量確保JWT中只包含必要的信息(大多數情況下只包含sub以及其他重要信息),對於敏感信息我們也應該省略掉從而防止XSS攻擊。JWT的核心在於聲明,聲明在JWT中是JSON數據,也就是說我們可以在JWT中嵌入用戶信息,從而減少數據庫負載。所以綜上所述JWT解決了其他會話存在的問題或缺點:
更靈活
更安全
減少數據庫往返,從而實現水平可伸縮。
防篡改客戶端聲明
移動設備上能更好工作
適用於阻止Cookie的用戶
綜上關於JWT在有效期內沒有強制使其無效的能力而完全否定JWT的好處顯然站不住腳,當然不可辯駁的是若是沒有如上諸多使用限制,實現其他類型的身份驗證完全也是合情合理且合法的,需綜合權衡,而非一家之言下死結論。到目前爲止,我們一直討論的是JWT VS Session認證,而不是JWT VS Cookie認證,但是如標題我們將Cookie也納入了,只是想讓學習者別搞混了,因爲JWT VS Cookie認證這種說法是錯誤的,Cookie只是一種存儲和傳輸信息介質,只能說我們可以通過Cookie存儲和傳輸JWT。接下來我們來實現Cookie存儲和傳輸JWT令牌。
JWT AS Cookies Identity Claim
在Startup中我們可以添加如下Cookie認證中間件,此時我們有必要了解下配置Cookie的一些選項,通過對這些選項的配置來告知Cookie身份認證中間件在瀏覽器中的表現形式,我們看下幾個涉及到安全的選項。
複製代碼
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
options.Cookie.Expiration = TimeSpan.FromMinutes(5);
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.None;
options.Cookie.SameSite = SameSiteMode.Lax;
});
複製代碼
配置HttpOnly標誌着Cookie是否僅供服務端使用,而不能通過前端直接訪問。
配置SecurePolicy將限制Cookie爲HTTPS,在生產環境建議配置此參數同時支持HTTPS。
配置SameSite用來指示瀏覽器是否可以將Cookie與跨站點請求一同使用,若是對於OAuth身份認證,可設置爲Lax,允許外部鏈接重定向發出比如POST請求而維持會話,若是Cookie認證,設置爲Restrict,因爲Cookie認證只適用於單站點,若是設置爲None,則不會設置Cookie Header值。(注意:SameSite屬性在谷歌、火狐瀏覽器均已實現,對於IE11好像不支持,Safari從版本12.1開始支持該屬性)
在創建.NET Core默認Web應用程序時,在ConfigureServices方法中,通過中間件直接配置了全局Cookie策略,如下:
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
當然默認配置了全局Cookie策略,同時也在Configure方法中使用其策略如下:
app.UseCookiePolicy();
我們也可以直接在上述調用使用Cookie策略中間件的方法中來設置對應參數策略,如下:
若是我們在添加Cookie中間件的同時也配置全局Cookie策略,我們會發現對於屬性HTTPOnly和SameSite都可配置,此時個人猜測會存在覆蓋的情況,如下:
對於需要認證的控制器我們需要添加上[Authroize]特性,對每一個控制器我們都得添加這樣一個特性,相信大部分童鞋都是這麼幹的。其實我們大可反向操作,對於無需認證的我們添加可匿名訪問特性即可,而需要認證的控制器我們進行全局配置認證過濾器,如下:
services.AddMvc(options=> options.Filters.Add(new AuthorizeFilter()))
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
好了到了這裏,我們只是粗略的講解了下關於Cookie中間件參數配置和Cookie全局配置策略的說明,沒有太深入去研究裏面的細枝末節,等遇到問題再具體分析吧。繼續回到話題,Cookie認證相比JWT對API訪問來講安全係數低,所以我們完全可以在Cookie認證中結合JWT來使用。具體我們可嘗試怎麼搞呢?將其放到身份信息聲明中,我想應該是可行的方式,我們來模擬登陸和登出試試,大概代碼如下:
複製代碼
public class AccountController : Controller
{
/// <summary>
/// 登錄
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> Login()
{
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, "Jeffcky"),
new Claim(JwtRegisteredClaimNames.Email, "[email protected]"),
new Claim(JwtRegisteredClaimNames.Sub, "D21D099B-B49B-4604-A247-71B0518A0B1C"),
new Claim("access_token", GenerateAccessToken()),
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authticationProperties = new AuthenticationProperties();
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authticationProperties);
return RedirectToAction(nameof(HomeController.Index), "Home");
}
string GenerateAccessToken()
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456"));
var token = new JwtSecurityToken(
issuer: "http://localhost:5000",
audience: "http://localhost:5001",
notBefore: DateTime.Now,
expires: DateTime.Now.AddHours(1),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
/// <summary>
/// 退出
/// </summary>
/// <returns></returns>
[Authorize]
[HttpPost]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction(nameof(HomeController.Index), "Home");
}
}
複製代碼
上述代碼很簡單,無需我再多講,和Cookie認證無異,只是我們在聲明中添加了access_token來提高安全性,接下來我們自定義一個Action過濾器特性,並將此特性應用於Action方法,如下:
複製代碼
public class AccessTokenActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var principal = context.HttpContext.User as ClaimsPrincipal;
var accessTokenClaim = principal?.Claims
.FirstOrDefault(c => c.Type == "access_token");
if (accessTokenClaim is null || string.IsNullOrEmpty(accessTokenClaim.Value))
{
context.HttpContext.Response.Redirect("/account/login", permanent: true);
return;
}
var sharedKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456"));
var validationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidIssuer = "http://localhost:5000",
ValidAudiences = new[] { "http://localhost:5001" },
IssuerSigningKeys = new[] { sharedKey }
};
var accessToken = accessTokenClaim.Value;
var handler = new JwtSecurityTokenHandler();
var user = (ClaimsPrincipal)null;
try
{
user = handler.ValidateToken(accessToken, validationParameters, out SecurityToken validatedToken);
}
catch (SecurityTokenValidationException exception)
{
throw new Exception($"Token failed validation: {exception.Message}");
}
base.OnActionExecuting(context);
}
}
複製代碼
JWT Combine Cookie Authentication
如上是採用將JWT放到聲明的做法,我想這麼做也未嘗不可,至少我沒找到這麼做有什麼不妥當的地方。我們也可以將Cookie認證和JWT認證進行混合使用,只不過是在上一節的基礎上添加了Cookie中間件罷了,如下圖:
通過如上配置後我們就可以將Cookie和JWT認證來組合使用了,比如我們在用戶登錄後,如下圖點擊登錄後顯示當前登錄用戶名,然後點擊退出,在退出Action方法上我們添加組合特性:
複製代碼
/// <summary>
/// 退出
/// </summary>
/// <returns></returns>
[Authorize(AuthenticationSchemes = "Bearer,Cookies")]
[HttpPost]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction(nameof(HomeController.Index), "Home");
}
複製代碼
在上一節中,我們通過獲取AccessToken,從而訪問端口號爲5001的客戶端來獲取當前時間,那現在我們針對獲取當前時間的方法添加上需要Cookie認證,如下:
複製代碼
[Authorize(CookieAuthenticationDefaults.AuthenticationScheme)]
[HttpGet("api/[controller]")]
public string GetCurrentTime()
{
var sub = User.FindFirst(d => d.Type == JwtRegisteredClaimNames.Sub)?.Value;
return DateTime.Now.ToString("yyyy-MM-dd");
}
複製代碼
Cookie認證撤銷
在.NET Core 2.1版本通過Cookie進行認證中,當用戶與應用程序進行交互修改了信息,需要在cookie的整個生命週期,也就說在註銷或cookie過期之前看不到信息的更改時,我們可通過cookie的身份認證事件【撤銷身份】來實現這樣的需求,下面我們來看看。
複製代碼
public class RevokeCookieAuthenticationEvents : CookieAuthenticationEvents
{
private readonly IDistributedCache _cache;
public RevokeCookieAuthenticationEvents(
IDistributedCache cache)
{
_cache = cache;
}
public override Task ValidatePrincipal(
CookieValidatePrincipalContext context)
{
var userId = context.Principal?.Claims
.First(c => c.Type == JwtRegisteredClaimNames.Sub)?.Value;
if (!string.IsNullOrEmpty(_cache.GetString("revoke-" + userId)))
{
context.RejectPrincipal();
_cache.Remove("revoke-" + userId);
}
return Task.CompletedTask;
}
}
複製代碼
我們通過重寫CookieAuthenticationEvents事件中的ValidatePrincipal,然後判斷寫在內存中關於用戶表示是否存在,若存在則調用 context.RejectPrincipal() 撤銷用戶身份。然後我們在添加Cookie中間件裏配置該事件類型以及對其進行註冊:
services.AddScoped();
接下來我們寫一個在頁面上點擊【修改信息】的方法,並在內存中設置撤銷指定用戶,如下:
複製代碼
[HttpPost]
public IActionResult ModifyInformation()
{
var principal = HttpContext?.User as ClaimsPrincipal;
var userId = principal?.Claims
.First(c => c.Type == JwtRegisteredClaimNames.Sub)?.Value;
if (!string.IsNullOrEmpty(userId))
{
_cache.SetString("revoke-" + userId, userId);
}
return RedirectToAction(nameof(HomeController.Index), "Home");
}
複製代碼
從如上動圖中我們可以看到,當點擊修改信息後,然後將撤銷的用戶標識寫入到內存中,然後跳轉到Index頁面,此時調用我們寫的撤銷事件,最終重定向到登錄頁,且此時用戶cookie仍未過期,所以我們能夠在左上角看到用戶名,不清楚這種場景在什麼情況下才會用到。
重定向至登錄攜帶或移除參數
當我們在某個頁面進行操作時,若此時Token或Cookie過期了,此時則會自動引導用戶且將用戶當前訪問的URL攜帶並重定向跳轉到登錄頁進行登錄,比如關於博客園如下跳轉URL:
https://account.cnblogs.com/signin?returnUrl=http%3a%2f%2fi.cnblogs.com%2f
但是如果我們有這樣的業務場景:用於跳轉至登錄頁時,在URL上需要攜帶額外的參數,我們需要獲取此業務參數才能進行對應業務處理,那麼此時我們應該如何做呢?我們依然是重寫CookieAuthenticationEvents事件中的RedrectToLogin方法,如下:
複製代碼
public class RedirectToLoginCookieAuthenticationEvents : CookieAuthenticationEvents
{
private IUrlHelperFactory _helper;
private IActionContextAccessor _accessor;
public RedirectToLoginCookieAuthenticationEvents(IUrlHelperFactory helper,
IActionContextAccessor accessor)
{
_helper = helper;
_accessor = accessor;
}
public override Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context)
{
//獲取路由數據
var routeData = context.Request.HttpContext.GetRouteData();
//獲取路由數據中的路由值
var routeValues = routeData.Values;
var uri = new Uri(context.RedirectUri);
//解析跳轉URL查詢參數
var returnUrl = HttpUtility.ParseQueryString(uri.Query)[context.Options.ReturnUrlParameter];
//add extra parameters for redirect to login
var parameters = $"id={Guid.NewGuid().ToString()}";
//添加額外參數到路由值中
routeValues.Add(context.Options.ReturnUrlParameter, $"{returnUrl}{parameters}");
var urlHelper = _helper.GetUrlHelper(_accessor.ActionContext);
context.RedirectUri = UrlHelperExtensions.Action(urlHelper, "Login", "Account", routeValues);
return base.RedirectToLogin(context);
}
}
複製代碼
這裏需要注意的是因爲上述我們用到了IActionContextAccessor,所以我們需要將其進行對應如下注冊:
services.AddSingleton();
最終我們跳轉到登錄頁將會看到我們添加的額外參數id也將呈現在url上,如下:
http://localhost:5001/Account/Login?ReturnUrl=%2FAccount%2FGetCurrentTime%3Fid%3Da309f451-e2ff-4496-bf18-65ba5c3ace9f
總結
本節我們講解了Session和JWT的優缺點以及Cookie認證中可能我們需要用到的地方,下一節也是JWT最後一節內容,我們講講並探討如何實現刷新Token,感謝閱讀。
原文地址https://www.cnblogs.com/CreateMyself/p/11197497.html