1. 程式人生 > >aspnet core 2.1中使用jwt從原理到精通一

aspnet core 2.1中使用jwt從原理到精通一

原生 context jti ide head tostring http請求 驗證 其他

目錄

  1. 原理;
  2. 根據原理使用C#語言,生成jwt;
  3. 自定義驗證jwt;
  4. 使用aspnetcore 中自帶的類生成jwt;

學有所得

  1. 了解jwt原理;
  2. 使用C#輕松實現jwt生成和驗證

原理

jwt對所有語言都是通用的,只要知道秘鑰,另一一種語言有可以對jwt的有效性進行判斷;

jwt的組成;Header部分Base64轉化.Payload部分Base64轉化.使用HS256方式根據秘鑰對前面兩部分進行加密後再Base64轉化,其中使用的hs256加密是header部分指定的,也可以通過官網的查看,如下圖:

技術分享圖片

原理就這麽簡單,那究竟用怎樣使用C#來實現呢,又怎麽確定它的正確性呢?,請繼續

使用C#實現

我們定義一個今天方法,其中需要使用到Microsoft.IdentityModel.Tokens.dll,asp.net core 2.1再帶,如果其他版本,沒有自帶,需要nuget 一下這個類庫

    /// <summary>
        /// 創建jwttoken,源碼自定義
        /// </summary>
        /// <param name="payLoad"></param>
        /// <param name="header"></param>
        /// <returns></returns>
public static string CreateToken(Dictionary<string, object> payLoad,int expiresMinute, Dictionary<string, object> header = null) { if (header == null) { header = new Dictionary<string, object>(new List<KeyValuePair<string
, object>>() { new KeyValuePair<string, object>("alg", "HS256"), new KeyValuePair<string, object>("typ", "JWT") }); } //添加jwt可用時間(應該必須要的) var now = DateTime.UtcNow; payLoad["nbf"] = ToUnixEpochDate( now);//可用時間起始 payLoad["exp"] = ToUnixEpochDate(now.Add(TimeSpan.FromMinutes(expiresMinute)));//可用時間結束 var encodedHeader = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(header)); var encodedPayload = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(payLoad)); var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey)); var encodedSignature = Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(encodedHeader, ".", encodedPayload)))); var encodedJwt = string.Concat(encodedHeader, ".", encodedPayload, ".", encodedSignature); return encodedJwt; }

public static long ToUnixEpochDate(DateTime date) =>
(long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);

 

該方法很簡單,只需要傳入header鍵值對和payLoad鍵值對,然後根據原理進行Base64轉換和hs256加密,接下來我們來使用一個測試類對其進行測試,代碼如下:

 [TestMethod]
        public void TokenValidateTest()
        {
            Dictionary<string, object> payLoad = new Dictionary<string, object>();
            payLoad.Add("sub", "rober");
            payLoad.Add("jti", "09e572c7-62d0-4198-9cce-0915d7493806");
            payLoad.Add("nbf", null);
            payLoad.Add("exp", null);
            payLoad.Add("iss", "roberIssuer");
            payLoad.Add("aud", "roberAudience");
            payLoad.Add("age", 30);

            var encodeJwt = TokenContext.CreateToken(payLoad, 30);

            var result = TokenContext.Validate(encodeJwt, (load) => { return true; });
            Assert.IsTrue(result);
        }

先不管後面的驗證,我們先看看其中生成的encodeJwt的值:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyb2JlciIsImp0aSI6IjY0OWMyYjUxLTE4ZGQtNDEzYy05Yzc5LTI4NWNhMDAxODU2NSIsIm5iZiI6MTU0MDYxMDY2NSwiZXhwIjoxNTQwNjEyNDY1LCJpc3MiOiJyb2Jlcklzc3VlciIsImF1ZCI6InJvYmVyQXVkaWVuY2UiLCJhZ2UiOjMwfQ.7Is2KYHAtSr5fW2gPU1jGeHPzz2ULCZJGcWb40LSYyw

技術分享圖片

第一部分和第二部分,並不是加密,只是Base64轉換,我們可以通過其他語言輕松轉換回來,如下使用javascript進行轉,window.atob(base64加密) window.btoa(base64解密)

var header=JSON.parse(window.atob(‘eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9‘))

如下圖:

技術分享圖片

我再對payLoa進行轉換回來, var payLoad=JSON.parse(window.atob(‘eyJzdWIiOiJyb2JlciIsImp0aSI6IjY0OWMyYjUxLTE4ZGQtNDEzYy05Yzc5LTI4NWNhMDAxODU2NSIsIm5iZiI6MTU0MDYxMDY2NSwiZXhwIjoxNTQwNjEyNDY1LCJpc3MiOiJyb2Jlcklzc3VlciIsImF1ZCI6InJvYmVyQXVkaWVuY2UiLCJhZ2UiOjMwfQ‘)) ,如下圖:

技術分享圖片

所以,從這裏可以看出來,Base64並不是屬於加密,只是簡單轉換,因此,不能在payLoad中存放重要內容,比如密碼等

使用aspnetcore 中自帶的類生成jwt

aspnet core中自帶了一個jwt幫助類,其實原理一樣,對上面做了封裝,豐富了一個內容,我們繼續使用一個靜態方法,如下

技術分享圖片
/// <summary>
        /// 創建jwtToken,采用微軟內部方法,默認使用HS256加密,如果需要其他加密方式,請更改源碼
        /// 返回的結果和CreateToken一樣
        /// </summary>
        /// <param name="payLoad"></param>
        /// <param name="expiresMinute">有效分鐘</param>
        /// <returns></returns>
        public static string CreateTokenByHandler(Dictionary<string, object> payLoad, int expiresMinute)
        {
           
            var now = DateTime.UtcNow;

            // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.
            // You can add other claims here, if you want:
            var claims = new List<Claim>();
            foreach (var key in payLoad.Keys)
            {
                var tempClaim = new Claim(key, payLoad[key]?.ToString());
                claims.Add(tempClaim);
            }
           

            // Create the JWT and write it to a string
            var jwt = new JwtSecurityToken(
                issuer: null,
                audience: null,
                claims: claims,
                notBefore: now,
                expires: now.Add(TimeSpan.FromMinutes(expiresMinute)),
                signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(securityKey)), SecurityAlgorithms.HmacSha256));
            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
            return encodedJwt;
        }
View Code

它效果和上面一模一樣,如果使用同樣的header 、payload、秘鑰,生成的jwt肯定一樣,這裏就不演示了,感興趣的可以自行嘗試;

aspnetcore中如何使用自定義jwt驗證

上面講了那麽多,只是為了大家更好的理解如何使用jwt進行驗證,那是jwt是如何進行驗證的呢?,如果一個http請求過來,一般jwt攜帶在http請求頭部的Authorization中;先不看如何獲取,先看看他是如何驗證的,我們再定義個靜態方法,如下:

 /// <summary>
        /// 驗證身份 驗證簽名的有效性,
        /// </summary>
        /// <param name="encodeJwt"></param>
        /// <param name="validatePayLoad">自定義各類驗證; 是否包含那種申明,或者申明的值, </param>
        /// 例如:payLoad["aud"]?.ToString() == "roberAuddience";
        /// 例如:驗證是否過期 等
        /// <returns></returns>
        public static bool Validate(string encodeJwt,Func<Dictionary<string,object>,bool> validatePayLoad)
        {
            var success = true;
            var jwtArr = encodeJwt.Split(.);
            var header = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[0]));
            var payLoad = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[1]));

            var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey));
            //首先驗證簽名是否正確(必須的)
            success = success && string.Equals(jwtArr[2], Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(jwtArr[0], ".", jwtArr[1])))));
            if (!success)
            {
                return success;//簽名不正確直接返回
            }
            //其次驗證是否在有效期內(也應該必須)
            var now = ToUnixEpochDate(DateTime.UtcNow);
            success = success && (now >= long.Parse(payLoad["nbf"].ToString()) && now < long.Parse(payLoad["exp"].ToString()));

            //再其次 進行自定義的驗證
            success = success && validatePayLoad(payLoad);

            return success;
        }

其中 validatePayLoad 參數是一個自定義的驗證的Fun,執行該Fun方法時會把解密後的payload作為參數傳入進去

我們驗證通過分為兩部分,

  1. 第一,必須的(自認為的)
    1.  jwt簽名是否正確,請看以上代碼實現
    2.  jwt是否在可以時間內,請看以上代碼實現
  2. 第二,自定義的(各復雜的,原理就是獲取payLoad 的某個值,然後對這個值進行各種判讀--等於,大於,包含,)
    1.   該jwt是不是進入黑名單
    2. aud==‘roberAudience’

我們來通過一個測試類驗證

 [TestMethod]
        public void TokenCustomerValidateTest()
        {
            Dictionary<string, object> payLoad = new Dictionary<string, object>();
            payLoad.Add("sub", "rober");
            payLoad.Add("jti", Guid.NewGuid().ToString());
            payLoad.Add("nbf", null);
            payLoad.Add("exp", null);
            payLoad.Add("iss", "roberIssuer");
            payLoad.Add("aud", "roberAudience");
            payLoad.Add("age", 30);

            var encodeJwt = TokenContext.CreateToken(payLoad, 30);

            var result = TokenContext.Validate(encodeJwt, (load) => {
                var success = true;
                //驗證是否包含aud 並等於 roberAudience
                success = success&& load["aud"]?.ToString() == "roberAudience";

                //驗證age>20等
                int.TryParse(load["age"].ToString(), out int age);
                Assert.IsTrue(age > 30);
                //其他驗證 jwt的標識 jti是否加入黑名單等

                return success;
            });
            Assert.IsTrue(result);
        }

如上面,我們可以把jwt中的payload解析出來,然後進行各種復雜的想要的驗證;

其實,aspnet core中的基於角色,用戶、策略,自定義策略的驗證就相當這裏的自定義驗證,一下章將詳細說明和對比,這裏暫時不講解

看完上面,是不是覺得jwt很簡單就,主要就兩部

  1. 創建jwt;
  2. 驗證jwt;

完整代碼如下:

技術分享圖片
 /// <summary>
    /// Token上下文,負責token的創建和驗證
    /// </summary>
    public class TokenContext
    {
        /// <summary>
        /// 秘鑰,可以從配置文件中獲取
        /// </summary>
        public static string securityKey = "GQDstclechengroberbojPOXOYg5MbeJ1XT0uFiwDVvVBrk";

        /// <summary>
        /// 創建jwttoken,源碼自定義
        /// </summary>
        /// <param name="payLoad"></param>
        /// <param name="header"></param>
        /// <returns></returns>
        public static string CreateToken(Dictionary<string, object> payLoad,int expiresMinute, Dictionary<string, object> header = null)
        {
            if (header == null)
            {
                header = new Dictionary<string, object>(new List<KeyValuePair<string, object>>() {
                    new KeyValuePair<string, object>("alg", "HS256"),
                    new KeyValuePair<string, object>("typ", "JWT")
                });
            }
            //添加jwt可用時間(應該必須要的)
            var now = DateTime.UtcNow;
            payLoad["nbf"] = ToUnixEpochDate( now);//可用時間起始
            payLoad["exp"] = ToUnixEpochDate(now.Add(TimeSpan.FromMinutes(expiresMinute)));//可用時間結束

            var encodedHeader = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(header));
            var encodedPayload = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(payLoad));

            var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey));
            var encodedSignature = Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(encodedHeader, ".", encodedPayload))));

            var encodedJwt = string.Concat(encodedHeader, ".", encodedPayload, ".", encodedSignature);
            return encodedJwt;
        }

        /// <summary>
        /// 創建jwtToken,采用微軟內部方法,默認使用HS256加密,如果需要其他加密方式,請更改源碼
        /// 返回的結果和CreateToken一樣
        /// </summary>
        /// <param name="payLoad"></param>
        /// <param name="expiresMinute">有效分鐘</param>
        /// <returns></returns>
        public static string CreateTokenByHandler(Dictionary<string, object> payLoad, int expiresMinute)
        {
           
            var now = DateTime.UtcNow;

            // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.
            // You can add other claims here, if you want:
            var claims = new List<Claim>();
            foreach (var key in payLoad.Keys)
            {
                var tempClaim = new Claim(key, payLoad[key]?.ToString());
                claims.Add(tempClaim);
            }
           

            // Create the JWT and write it to a string
            var jwt = new JwtSecurityToken(
                issuer: null,
                audience: null,
                claims: claims,
                notBefore: now,
                expires: now.Add(TimeSpan.FromMinutes(expiresMinute)),
                signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(securityKey)), SecurityAlgorithms.HmacSha256));
            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
            return encodedJwt;
        }

        /// <summary>
        /// 驗證身份 驗證簽名的有效性,
        /// </summary>
        /// <param name="encodeJwt"></param>
        /// <param name="validatePayLoad">自定義各類驗證; 是否包含那種申明,或者申明的值, </param>
        /// 例如:payLoad["aud"]?.ToString() == "roberAuddience";
        /// 例如:驗證是否過期 等
        /// <returns></returns>
        public static bool Validate(string encodeJwt,Func<Dictionary<string,object>,bool> validatePayLoad)
        {
            var success = true;
            var jwtArr = encodeJwt.Split(.);
            var header = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[0]));
            var payLoad = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[1]));

            var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey));
            //首先驗證簽名是否正確(必須的)
            success = success && string.Equals(jwtArr[2], Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(jwtArr[0], ".", jwtArr[1])))));
            if (!success)
            {
                return success;//簽名不正確直接返回
            }
            //其次驗證是否在有效期內(也應該必須)
            var now = ToUnixEpochDate(DateTime.UtcNow);
            success = success && (now >= long.Parse(payLoad["nbf"].ToString()) && now < long.Parse(payLoad["exp"].ToString()));

            //再其次 進行自定義的驗證
            success = success && validatePayLoad(payLoad);

            return success;
        }
        /// <summary>
        /// 獲取jwt中的payLoad
        /// </summary>
        /// <param name="encodeJwt"></param>
        /// <returns></returns>
        public static Dictionary<string ,object> GetPayLoad(string encodeJwt)
        {
            var jwtArr = encodeJwt.Split(.);
            var payLoad = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[1]));
            return payLoad;
        }
        public static long ToUnixEpochDate(DateTime date) => 
            (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);
    }
View Code

以上就是jwt的基本內容,它確實很簡單,不要被aspnet core中的各種寫法給搞暈了,只要是jwt相關的驗證都是基於上面這些東西

下一章節將講述:

  1. 在aspnet core中,自定義jwt管道驗證;
  2. 在aspnet core中,自定義策略驗證CommonAuthorizeHandler : AuthorizationHandler<CommonAuthorize>
  3. 自定義jwt邏輯驗證和原生的角色,用戶,策略,等進行對比

aspnet core 2.1中使用jwt從原理到精通一