從零開始搭建前後端分離的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的項目框架之七使用JWT生成Token(個人見解)

来源:https://www.cnblogs.com/levywang/archive/2019/09/05/coreframe_7.html
-Advertisement-
Play Games

在 上一篇 中講到了在NetCore項目中如何進行全局的請求模型驗證,只要在請求模型中加了驗證特性,介面使用時只用將數據拿來使用,而不用去關係數據是否符合業務需求。 這篇中將講些個人對於JWT的看法和使用,在網上也能找到很多相關資料和如何使用,基本都是直接嵌到 Startup 類中來單獨使用。而博主 ...


  在 上一篇 中講到了在NetCore項目中如何進行全局的請求數據模型驗證,只要在請求模型中加了驗證特性,介面使用時只用將數據拿來使用,而不用去關心數據是否符合業務需求。

  這篇中將講些個人對於JWT的看法和使用,在網上也能找到很多相關資料和如何使用,基本都是直接嵌到  Startup 類中來單獨使用。而博主是將jwt當做一個驗證方法來使用。使用起來更加方便,並且在做驗證時也更加的靈活。

 1.什麼是JWT?

  Json web token (JWT), 是為了在網路應用環境間傳遞聲明而執行的一種基於JSON的開放標準((RFC 7519)。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源伺服器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證。

傳統的session認證

我們知道,http協議本身是一種無狀態的協議,而這就意味著如果用戶向我們的應用提供了用戶名和密碼來進行用戶認證,那麼下一次請求時,用戶還要再一次進行用戶認證才行,因為根據http協議,我們並不能知道是哪個用戶發出的請求,所以為了讓我們的應用能識別是哪個用戶發出的請求,我們只能在伺服器存儲一份用戶登錄的信息,這份登錄信息會在響應時傳遞給瀏覽器,告訴其保存為cookie,以便下次請求時發送給我們的應用,這樣我們的應用就能識別請求來自哪個用戶了,這就是傳統的基於session認證。

但是這種基於session的認證使應用本身很難得到擴展,隨著不同客戶端用戶的增加,獨立的伺服器已無法承載更多的用戶,而這時候基於session認證應用的問題就會暴露出來.

基於session認證所顯露的問題

Session: 每個用戶經過我們的應用認證之後,我們的應用都要在服務端做一次記錄,以方便用戶下次請求的鑒別,通常而言session都是保存在記憶體中,而隨著認證用戶的增多,服務端的開銷會明顯增大。

擴展性: 用戶認證之後,服務端做認證記錄,如果認證的記錄被保存在記憶體中的話,這意味著用戶下次請求還必須要請求在這台伺服器上,這樣才能拿到授權的資源,這樣在分散式的應用上,相應的限制了負載均衡器的能力。這也意味著限制了應用的擴展能力。

CSRF: 因為是基於cookie來進行用戶識別的, cookie如果被截獲,用戶就會很容易受到跨站請求偽造的攻擊。

基於token的鑒權機制

基於token的鑒權機制類似於http協議也是無狀態的,它不需要在服務端去保留用戶的認證信息或者會話信息。這就意味著基於token認證機制的應用不需要去考慮用戶在哪一臺伺服器登錄了,這就為應用的擴展提供了便利。

流程上是這樣的:

    • 用戶使用用戶名密碼來請求伺服器
    • 伺服器進行驗證用戶的信息
    • 伺服器通過驗證發送給用戶一個token
    • 客戶端存儲token,併在每次請求時附送上這個token值
    • 服務端驗證token值,並返回數據

JWT的構成

第一部分我們稱它為頭部(header),第二部分我們稱其為載荷(payload, 類似於飛機上承載的物品),第三部分是簽證(signature).

header

jwt的頭部承載兩部分信息:

    • 聲明類型,這裡是jwt
    • 聲明加密的演算法 通常直接使用 HMAC SHA256

完整的頭部就像下麵這樣的JSON:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

然後將頭部進行base64加密(該加密是可以對稱解密的),構成了第一部分.

playload

載荷就是存放有效信息的地方。這個名字像是特指飛機上承載的貨品,這些有效信息包含三個部分

    • 標準中註冊的聲明
    • 公共的聲明
    • 私有的聲明

標準中註冊的聲明 (建議但不強制使用) :

    • iss: jwt簽發者
    • sub: jwt所面向的用戶
    • aud: 接收jwt的一方
    • exp: jwt的過期時間,這個過期時間必須要大於簽發時間
    • nbf: 定義在什麼時間之前,該jwt都是不可用的.
    • iat: jwt的簽發時間
    • jti: jwt的唯一身份標識,主要用來作為一次性token,從而迴避重放攻擊。

公共的聲明 :
公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因為該部分可以直接base64解碼,可以看到裡面的信息

signature

jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:

    • header (base64後的)
    • payload (base64後的)
    • secret

這個部分需要base64加密後的header和base64加密後的payload使用.連接組成的字元串,然後通過header中聲明的加密方式進行加鹽secret組合加密,然後就構成了jwt的第三部分。

將這三部分用 . 連接成一個完整的字元串,構成了最終的jwt。

註意:secret是保存在伺服器端的,jwt的簽發生成也是在伺服器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味著客戶端是可以自我簽發jwt了。

2.如何將JWT 脫離出來生成與驗證?

  在任意類庫(建議放在公用類中)的NuGet包管理中添加: System.IdentityModel.Tokens.Jwt  然後添加  TokenManager 類

    /// <summary>
    /// token管理類
    /// </summary>
    public class TokenManager
    {
        //私有欄位建議放到配置文件中
        /// <summary>
        /// 秘鑰  4的倍數  長度大於等於24
        /// </summary>
        private static string _secret = "levy0102030405060708asdf";
        /// <summary>
        /// 發佈者
        /// </summary>
        private static string _issuer = "levy";

        /// <summary>
        /// 生成token
        /// </summary>
        /// <param name="tokenStr">需要簽名的數據  </param>
        /// <param name="expireHour">預設3天過期</param>
        /// <returns>返回token字元串</returns>
        public static string GenerateToken(string tokenStr, int expireHour = 3 * 24) //3天過期
        {
            var key1 = new SymmetricSecurityKey(Convert.FromBase64String(_secret));
            var cred = new SigningCredentials(key1, SecurityAlgorithms.HmacSha256);
            var claims = new[]
            {
                new Claim("sid",tokenStr),
                //new Claim(ClaimTypes.Name,name), //示例  可使用ClaimTypes中的類型
            };
            var token = new JwtSecurityToken(
                issuer: _issuer,//簽發者
                notBefore: DateTime.Now,//token不能早於這個時間使用
                expires: DateTime.Now.AddHours(expireHour),//添加過期時間
                claims: claims,//簽名數據
                signingCredentials: cred//簽名
                );
            //解決一個不知什麼問題的PII什麼異常
            IdentityModelEventSource.ShowPII = true;
            return new JwtSecurityTokenHandler().WriteToken(token);
        }

        /// <summary>
        /// 得到Token中的驗證消息
        /// </summary>
        /// <param name="token"></param>
        /// <param name="dateTime"></param>
        /// <returns></returns>
        public static string ValidateToken(string token, out DateTime dateTime)
        {
            dateTime = DateTime.Now;
            var principal = GetPrincipal(token, out dateTime);

            if (principal == null)
                return default(string);

            ClaimsIdentity identity = null;
            try
            {
                identity = (ClaimsIdentity)principal.Identity;
            }
            catch (NullReferenceException)
            {
                return null;
            }
            //identity.FindFirst(ClaimTypes.Name).Value;
            return identity.FindFirst("sid").Value;
        }

        /// <summary>
        /// 從Token中得到ClaimsPrincipal對象
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        private static ClaimsPrincipal GetPrincipal(string token, out DateTime dateTime)
        {
            try
            {
                dateTime = DateTime.Now;
                var tokenHandler = new JwtSecurityTokenHandler();
                var jwtToken = (JwtSecurityToken)tokenHandler.ReadToken(token);

                if (jwtToken == null)
                    return null;

                var key = Convert.FromBase64String(_secret);

                var parameters = new TokenValidationParameters()
                {
                    RequireExpirationTime = true,
                    ValidateIssuer = true,//驗證創建該令牌的發佈者
                    ValidateLifetime = true,//檢查令牌是否未過期,以及發行者的簽名密鑰是否有效
                    ValidateAudience = false,//確保令牌的接收者有權接收它
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidIssuer = _issuer//驗證創建該令牌的發佈者
                };
                //驗證token 
                var principal = tokenHandler.ValidateToken(token, parameters, out var securityToken);
                //若開始時間大於當前時間 或結束時間小於當前時間 則返回空
                if (securityToken.ValidFrom.ToLocalTime() > DateTime.Now || securityToken.ValidTo.ToLocalTime() < DateTime.Now)
                {
                    dateTime = DateTime.Now;
                    return null;
                }
                dateTime = securityToken.ValidTo.ToLocalTime();//返回Token結束時間
                return principal;
            }
            catch (Exception e)
            {
                dateTime = DateTime.Now;
                LogHelper.Logger.Fatal(e, "Token驗證失敗");
                return null;
            }
        }
    }

  再到控制器中添加測試方法

        [HttpGet]
        [Route("testtoken")]
        public ActionResult TestToken()
        {
            var token = TokenManager.GenerateToken("測試token的生成");
            Response.Headers["token"] = token;
            Response.Headers["Access-Control-Expose-Headers"] = "token";//一定要添加這一句  不然前端是取不到token欄位的值的!更別提存store了。
            return Succeed(token);
        }

在這裡必須得提的地方是  若是前後端分離的項目,由於存在跨域問題,必須得在返回header中多添加一個欄位 Access-Control-Expose-Headers 該欄位對應的值為前端需要取得欄位的集合,以英文逗號分隔。

原因:在跨域訪問時,XMLHttpRequest對象的getResponseHeader()方法只能拿到一些最基本的響應頭,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要訪問其他頭,則需要伺服器設置本響應頭。Access-Control-Expose-Headers 頭讓伺服器把允許瀏覽器訪問的頭放入白名單。不然容易出現前後端開發人員撕逼哦~

測試結果截圖:

能看到數據能返回出來,在調試中也能看到。接著拿這個去訪問介面。博主這裡只是示例,具體業務視情況而定。

接下來我們拿生成的token去訪問驗證下是否能成功,在驗證token的時候我們可以順帶看下token是否即將過期,若快要過期了就取一個新的token。當然這裡有一個問題就是之前的token還可以使用。這裡可以用其它手段來規避。如緩存過期token判斷等。

        [HttpPost]
        [Route("validtoken")]
        public ActionResult ValidToken([FromHeader]string token)
        {
            var str = TokenManager.ValidateToken(token, out DateTime date);
            if (!string.IsNullOrEmpty(str) || date > DateTime.Now)
            {
                //當token過期時間小於五小時,更新token並重新返回新的token
                if (date.AddHours(-5) > DateTime.Now) return Succeed($"Token字元串:{str},過期時間:{date}");
                var nToken = TokenManager.GenerateToken(str);
                Response.Headers["token"] = nToken;
                token = nToken;
                Response.Headers["Access-Control-Expose-Headers"] = "token";
            }
            else
            {
                return Fail(101, "未取得授權信息");
            }
            return Succeed($"Token字元串:{str},過期時間:{DateTime.Now.AddHours(3 * 24)}");
        }

  測試結果:

 

 3.問題與討論~ 

  JWT也存在很多疑問的地方,比如 1.被盜取了怎麼辦?2.用戶處於失控狀態下?等等問題。

  建議:1.不在payload部分存放敏感信息,且儘可能使用https方式,防止被盜的可能性,且提醒用戶有風險,不要在公共地方登陸。提供給用戶token保存時間選擇,若未選擇長期保存則只存sessionStorage ,選了則存localStorage。

       2.後端用戶信息一般存於緩存之中,一般用戶使用時間不會太長,所以後端緩存設置時間短(如2小時),當後端緩存過期了就根據payload部分數據來取用戶信息存緩存, 用戶信息添加穩定狀態值來判斷是否可用。

       3.為解決2要使用payload部分的數據,為防止泄露,可進行AES 進行加密處理,當需要使用時取出在解密使用。

  以上屬個人想法。有什麼問題歡迎提出,共同討論。

 

  後續補充:

  後端使用:只需要新建 BaseUserController 來繼承 BaseController  重寫 OnActionExecuting 方法,在該方法中添加驗證判斷。如果在控制器中有某個介面不需要驗證,但是又繼承了 BaseUserController  的話,

  可以在介面方法上加上 AllowAnonymousAttribute 屬性來排除驗證。    沒有使用刷新和驗證token的區分,個人覺得這兩者都存在一樣的問題,何不就用一個呢?

  BaseUserController 類代碼

  

/// <summary>
    /// 用戶許可權驗證控制器
    /// </summary>
    public abstract class BaseUserController : BaseController
    {
//        private UserModel _user;
//        /// <summary>
//        /// 當前用戶
//        /// </summary>
//        protected new UserModel User
//        {
//            get => _user ?? (_user = _userCache.Current);//從緩存中取
//            set => _user = value;
//        }

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            base.OnActionExecuting(filterContext);

            if (filterContext.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
            {
                var isDefined = controllerActionDescriptor.MethodInfo.GetCustomAttributes(true)
                    .Any(a => a.GetType() == typeof(AllowAnonymousAttribute));
                if (isDefined)
                {
                    return;
                }
            }

            var token = Request.Headers["token"];
            if (string.IsNullOrEmpty(token))
            {
                filterContext.Result = new CustomHttpStatusCodeResult(200, 401, "未授權");
                return;
            }
            var str = TokenManager.ValidateToken(token, out DateTime date);
            if (!string.IsNullOrEmpty(str) || date > DateTime.Now)
            {
                //當token過期時間小於五小時,更新token並重新返回新的token
                if (date.AddHours(-5) > DateTime.Now) return;
                var nToken = TokenManager.GenerateToken(str);
                Response.Headers["token"] = nToken;
                Response.Headers["Access-Control-Expose-Headers"] = "token";
                return;
            }

            filterContext.Result = new CustomHttpStatusCodeResult(200, 401, "未授權");
        }
    }

  添加token測試代碼,將之前的測試代碼改變下

public class TokenTestController : BaseUserController
    {
        [HttpGet]
        [Route("testtoken")]
        [AllowAnonymous]//允許所有人訪問
        public ActionResult TestToken()
        {
            var token = TokenManager.GenerateToken("測試token的生成");
            Response.Headers["token"] = token;
            Response.Headers["Access-Control-Expose-Headers"] = "token";//一定要添加這一句  不然前端是取不到token欄位的值的!更別提存store了。
            return Succeed(token);
        }

        //[HttpPost]
        //[Route("validtoken")]
        //public ActionResult ValidToken([FromHeader]string token)
        //{
        //    var str = TokenManager.ValidateToken(token, out DateTime date);
        //    if (!string.IsNullOrEmpty(str) || date > DateTime.Now)
        //    {
        //        //當token過期時間小於五小時,更新token並重新返回新的token
        //        if (date.AddHours(-5) > DateTime.Now) return Succeed($"Token字元串:{str},過期時間:{date}");
        //        var nToken = TokenManager.GenerateToken(str);
        //        Response.Headers["token"] = nToken;
        //        token = nToken;
        //        Response.Headers["Access-Control-Expose-Headers"] = "token";
        //    }
        //    else
        //    {
        //        return Fail(101, "未取得授權信息");
        //    }
        //    return Succeed($"Token字元串:{str},過期時間:{DateTime.Now.AddHours(3 * 24)}");
        //}
        [HttpPost]
        [Route("validtoken")]
        public ActionResult ValidToken()
        {
            //業務處理  token已在基類中驗證
            return Succeed("成功");
        }
    }

  然後再允許測試看下效果。發現是不是特別棒~~~~

  

  前端使用:使用Axios來管理執行請求操作。可以完美的使用請求、響應攔截器來處理token等信息。做業務時都無需關心token問題。

  

  部分文字描述參考於:

  https://www.jianshu.com/p/576dbf44b2ae

  

  在下一篇中將介紹如何在NetCore中如何使用 MemoryCache 和 Redis 來做緩存不常變動數據,提高響應速度~~

 

  有需要源碼的在下方評論或私信~給我的SVN訪客賬戶密碼下載,代碼未放在GitHub上。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 今日所學: /* 2019.08.19開始學習,此為補檔。 */ ①char:只能有一個欄位。字元:' ' ②二進位:0000 0000 最後一位為0就不算,為1代表20。 如25為:0001 1001 24+23+20 = 16+8+1 = 25 ③final int A = 45; final聲 ...
  • 內容: 變數 字元串 數字和運算符 數據類型轉換 一、變數 1.定義:指在電腦編程中與關聯的標識符配對的記憶體存儲位置,在使用時含相關類型的值,該值可以修改。 當Python變數被使用時,在記憶體里會產生兩個動作:1.開闢新的記憶體地址;2.賦予指定的變數值 註意:變數被指定的同時,必須強制賦初始值,否 ...
  • NumPy 不會主動檢測並利用 GPU,雖然 NumPy 已經很好用了,但是能利用 GPU 的 NumPy 替代品速度會更快。 ...
  • drf源碼save以及response 一.save python if self.instance is not None: self.instance = self.update(self.instance, validated_data) assert self.instance is not ...
  • 最近,項目提出需求,日誌需要固定輸出為 格式,以便後端 程式解析. 項目背景 項目為簡單的 項目,日誌由 採集,因此不需要配置輸出至 . 下麵為 文件中配置的依賴,此處使用 完成日誌格式轉換操作. Logback配置 上述即為 文件中的配置內容,需要註意日誌中的 從配置文件 中獲取,而 屬性則同 字 ...
  • #include <cstdio> #include <cmath> using namespace std; void dg(int n) { int a; if(n==0) return; for(int i=0;i<=15;i++) //求出n以內2最大的a次方 { a=i; if(pow(2... ...
  • 過濾器的位置是在用戶提交之後,攔截器攔截的位置是在handlermapping之後,攔截器需要在springmvc的配置文件中進行配置,攔截的那些文件 一、springmvc配置 <bean class="com.zhiyou100.zj.inter.Myinter"></bean>定義的過濾器的全 ...
  • jdk 安裝包 https://pan.baidu.com/s/1cKnUQGU2Sk2nsARAzzVAHw [root@localhost ~]# tar -zxvf jdk-8u152-linux-x64.tar.gz [root@localhost ~]# mv jdk1.8.0_152/ ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...