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


  在 上一篇 中講到了在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 來做緩存不常變動數據,提高響應速度~~

 

  有需要源碼的可通過此 GitHub 鏈接拉取 覺得還可以的給個 start 哦,謝謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM