ASP.NET Core Web Api之JWT VS Session VS Cookie(二)

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章