1. 程式人生 > >JWT認證

JWT認證

1.什麼是JWT Token

  JWT(Json Web Tokens) 是一個開放標準(RFC 7519),它定義了一種簡潔,自包含,JSON 物件形式的安全傳遞資訊的方法。JWT常用在 Web 應用或者移動應用上,Token是令牌的意思,表示只有拿著令牌才具有一些許可權。JWT的宣告(Claim)一般被用來在身份提供者和服務提供者間傳遞身份驗證資訊,也可以增加一些額外的其它業務邏輯所必須的宣告資訊。

JWT的使用場景

一次性驗證

比如使用者註冊後需要發一封郵件讓其啟用賬戶,通常郵件中需要有一個連結,這個連結需要具備以下的特性:能夠標識使用者,該連結具有時效性(通常只允許幾小時之內啟用),不能被篡改以啟用其他可能的賬戶…這種場景就和 jwt 的特性非常貼近,jwt 的 payload 中固定的引數:iss 簽發者和 exp 過期時間正是為其做準備的。

restful api 的無狀態認證

使用 jwt 來做 restful api 的身份認證也是值得推崇的一種使用方案。客戶端和服務端共享 secret;過期時間由服務端校驗,客戶端定時重新整理

2.JWT的組成

使用JWT token認證前我們先了解下JWT的組成部分。JWT經過加密處理與校驗處理的字串,形式 A.B.C

A由JWT頭部資訊header加密得到
B由JWT用到的身份驗證資訊json資料加密得到
C由A和B加密得到,是校驗部分

怎麼計算A?

header格式為:
{
    "typ": "JWT",
    "alg": "HS256" 
}

它就是一個json串,兩個欄位是必須的,不能多也不能少。alg欄位指定了生成C的演算法,預設值是HS256,將header用base64加密,得到A。
補充:Base64是一種表示二進位制資料的方法。由於2的6次方等於64,所以每6個位元為一個單元,對應某一個可列印字元。三個位元組有24個位元,對應於4個Base64單元,即3個位元組需要用4個Base64可列印字元來表示。

怎樣計算B?

  根據JWT claim set[用base64]加密得到的。claim set是一個json資料,是表明使用者身份的資料,可自行指定欄位很靈活,也有固定欄位表示特定含義(但不一定要包含特定欄位,只是推薦)。
一些欄位的含義:

    {
     
"iss" :"http://example.org", //非必須。issuer 請求實體,可以是發起請求的使用者的資訊,也可是jwt的簽發者。   "iat" : 1356999524, //非必須。issued at。 token建立時間,unix時間戳格式   "exp" :"1548333419", //非必須。expire 指定token的生命週期。unix時間戳格式   "aud" : "http://example.com", //非必須。接收該JWT的一方。   "sub" : "[email protected]", //非必須。該JWT所面向的使用者   "nbf" : 1357000000, //非必須。not before。如果當前時間在nbf裡的時間之前,則Token不被接受;一般都會留一些餘地,比如幾分鐘。   "jti" :'222we', //非必須。JWT ID。針對當前token的唯一標識   "GivenName" : "Jonny", // 自定義欄位   "Surname" : "Rocket", // 自定義欄位   "Email" : "[email protected]", // 自定義欄位   "Role" : ["Manager", "Project Administrator"] // 自定義欄位
  }

自定義欄位的key是一個string,value是一個json資料。將claim set通過Base64加密後得到B,學名payload(載荷)

怎樣計算C?

A.B使用HS256加密(其實是用header中指定的演算法),當然加密過程中還需要金鑰(secret,自行指定的一個字串)。加密得到C,學名signature,其實就是一個字串。

3.JWT的工作過程

借鑑於:https://www.cnblogs.com/lonelyxmas/p/8024006.html

下面我們從一個例項來看如何運用JWT機制實現認證:

1.登入

  • 第一次認證:第一次登入,使用者從瀏覽器輸入使用者名稱/密碼,提交後到伺服器的登入處理的Action層(Login Action);
  • Login Action呼叫認證服務進行使用者名稱密碼認證,如果認證通過,Login Action層呼叫使用者資訊服務獲取使用者資訊(包括完整的使用者資訊及對應許可權資訊);
  • 返回使用者資訊後,Login Action從配置檔案中獲取Token簽名生成的祕鑰資訊,進行Token的生成;
  • 生成Token的過程中可以呼叫第三方的JWT Lib生成簽名後的JWT資料;
  • 完成JWT資料簽名後,將其設定到COOKIE物件中,並重定向到首頁,完成登入過程;

2.請求認證

  基於Token的認證機制會在每一次請求中都帶上完成簽名的Token資訊,這個Token資訊可能在Cookie中,也可能在HTTP的Authorization頭中;

    • 客戶端(APP客戶端或瀏覽器)通過GET或POST請求訪問資源(頁面或呼叫API);
    • 認證服務作為一個Middleware HOOK 對請求進行攔截,首先在cookie中查詢Token資訊,如果沒有找到,則在HTTP Authorization Head中查詢;
    • 如果找到Token資訊,則根據配置檔案中的簽名加密祕鑰,呼叫JWT Lib對Token資訊進行解密和解碼;
    • 完成解碼並驗證簽名通過後,對Token中的exp、nbf、aud等資訊進行驗證;
    • 全部通過後,根據獲取的使用者的角色許可權資訊,進行對請求的資源的許可權邏輯判斷;
    • 如果許可權邏輯判斷通過則通過驗證,開始執行功能程式碼;否則則返回HTTP 401;

3.JWT.Net的使用

使用JWT.Net前要首先通過Nuge(git地址:https://github.com/jwt-dotnet/jwt)t獲取JWT.Net包,如下:

添加了JWT.Net包後就可以使用JWT了,這裡封裝了一個簡單的JWTHelper,程式碼如下:

    public class JWTHelper
    {
        static IJwtAlgorithm algorithm = new HMACSHA256Algorithm();//HMACSHA256加密
        static IJsonSerializer serializer = new JsonNetSerializer();//序列化和反序列
        static IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();//Base64編解碼
        static IDateTimeProvider provider = new UtcDateTimeProvider();//UTC時間獲取
        #region /////生成JWT
        public static string GetJWT(string secret, Dictionary<string, object> payload)
        {
            IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
            return encoder.Encode(payload, secret);
        }
        #endregion


        #region /////驗證JWT
        public static bool ValidateJwt(string secret, string token, out string payload,out string message)
        {
            bool isValidted = false;
            payload = "";
            try
            {
                IJwtValidator validator = new JwtValidator(serializer, provider);//用於驗證JWT的類
                IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder);//用於解析JWT的類
                payload = decoder.Decode(token, secret, verify: true);//verify:true表示解析JWT時進行驗證,該方法會自動呼叫validator的Validate()方法,不滿足驗證會丟擲異常,因此我們不用寫驗證的方法
                isValidted = true;
                message = "驗證成功";
            }
            catch (TokenExpiredException)//如果當前時間大於負載中的過期時間(負荷中的exp),引發Token過期異常
            {

                message = "Token已經過期了!";
            }
            catch (SignatureVerificationException)//如果簽名不匹配,引發簽名驗證異常
            {
                message = "Token簽名不正確!";
            }
            return isValidted;
        } 
        #endregion
    }

 我們在一個控制檯程式中簡單展示一下JWT.Net的用法,程式碼如下:

        static void Main(string[] args)
        {
            //服務端的祕鑰,一般放在配置檔案中
            const string secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
            //負荷(payload)
            var payload = new Dictionary<string, object>
            {
                { "claim1", 0 },
                { "testStr", "hello" },
                {"testObj" ,new { name="111"} },
                { "exp", DateTimeOffset.UtcNow.AddSeconds(2).ToUnixTimeSeconds() }
            };

            Console.WriteLine("生成JWT--------------------------------------------------------------");
            Console.WriteLine();
            string token = JWTHelper.GetJWT(secret, payload);
            Console.WriteLine($"生成的JWT是:{token}");
            Console.WriteLine();
            Console.WriteLine();
            Console.WriteLine();

            Console.WriteLine("校驗JWT--------------------------------------------------------------");
            Console.WriteLine();
            string message;//解析的訊息
            string curPayload;//解析獲取的負載
            if (JWTHelper.ValidateJwt(secret,token,out curPayload,out message))
            {
                Console.WriteLine($"解析獲取的負載是:{curPayload}");
            }
            Console.WriteLine(message);
        }
    }

執行結果如圖所示:

4.一些需要注意的問題

這些問題摘自https://blog.csdn.net/qq_28165595/article/details/80214994

1.jwt token洩露了怎麼辦? 

  前面的文章下有不少人留言提到這個問題,我則認為這不是問題。傳統的 session+cookie 方案,如果洩露了 sessionId,別人同樣可以盜用你的身份。揚湯止沸不如釜底抽薪,不妨來追根溯源一下,什麼場景會導致你的 jwt 洩露。 
遵循如下的實踐可以儘可能保護你的 jwt 不被洩露:使用 https 加密你的應用,返回 jwt 給客戶端時設定 httpOnly=true 並且使用 cookie 而不是 LocalStorage 儲存 jwt,這樣可以防止 XSS 攻擊和 CSRF 攻擊

2. secret如何設計

  JWT唯一儲存在服務端的只有一個 secret,個人認為這個 secret 應該設計成和使用者相關的屬性,而不是一個所有使用者公用的統一值。這樣可以有效的避免一些登出和修改密碼時遇到的窘境。 
登出和修改密碼 

  傳統的 session+cookie 方案使用者點選登出,服務端清空 session 即可,因為狀態儲存在服務端。但 jwt 的方案就比較難辦了,因為 jwt 是無狀態的,服務端通過計算來校驗有效性。沒有儲存起來,所以即使客戶端刪除了 jwt,但是該 jwt 還是在有效期內,只不過處於一個遊離狀態。分析下痛點:登出變得複雜的原因在於 jwt 的無狀態。我提供幾個方案,視具體的業務來決定能不能接受。 

  1. 僅僅清空客戶端的 cookie,這樣使用者訪問時就不會攜帶 jwt,服務端就認為使用者需要重新登入。這是一個典型的假登出,對於使用者表現出退出的行為,實際上這個時候攜帶對應的 jwt 依舊可以訪問系統。 
  2. 清空或修改服務端的使用者對應的 secret,這樣在使用者登出後,jwt 本身不變,但是由於 secret 不存在或改變,則無法完成校驗。這也是為什麼將 secret 設計成和使用者相關的原因。 
  3. 藉助第三方儲存自己管理 jwt 的狀態,可以以 jwt 為 key,實現去 redis 一類的快取中介軟體中去校驗存在性。方案設計並不難,但是引入 redis 之後,就把無狀態的 jwt 硬生生變成了有狀態了,違背了 jwt 的初衷。實際上這個方案和 session 都差不多了。 
修改密碼則略微有些不同,假設號被到了,修改密碼(是使用者密碼,不是 jwt 的 secret)之後,盜號者在原 jwt 有效期之內依舊可以繼續訪問系統,所以僅僅清空 cookie 自然是不夠的,這時,需要強制性的修改 secret。在我的實踐中就是這樣做的。 

3.續簽問題

  續約問題可以說是我抵制使用 jwt 來代替傳統 session 的最大原因,因為 jwt 的設計中我就沒有發現它將續簽認為是自身的一個特性。傳統的 cookie 續簽方案一般都是框架自帶的,session 有效期 30 分鐘,30 分鐘內如果有訪問,session 有效期被重新整理至 30 分鐘。而 jwt 本身的 payload 之中也有一個 exp 過期時間引數,來代表一個 jwt 的時效性,而 jwt 想延期這個 exp 就有點身不由己了,因為 payload 是參與簽名的,一旦過期時間被修改,整個 jwt 串就變了,jwt 的特性天然不支援續簽! 
如果你一定要使用 jwt 做會話管理(payload 中儲存會話資訊),也不是沒有解決方案,但個人認為都不是很令人滿意 
1.每次請求重新整理JWT
  JWT修改 payload 中的 exp 後整個 jwt 串就會發生改變,那…就讓它變好了,每次請求都返回一個新的 jwt 給客戶端。太暴力了,不用我贅述這樣做是多麼的不優雅,以及帶來的效能問題。但,至少這是最簡單的解決方案。 
2.只要快要過期的時候重新整理JWT
  一個上述方案的改造點是,只在最後的幾分鐘返回給客戶端一個新的 jwt。這樣做,觸發重新整理 jwt 基本就要看運氣了,如果使用者恰巧在最後幾分鐘訪問了伺服器,觸發了重新整理,萬事大吉;如果使用者連續操作了 27 分鐘,只有最後的 3 分鐘沒有操作,導致未重新整理 jwt,無疑會令使用者抓狂。 
3.完善 refreshToken 
  借鑑 oauth2 的設計,返回給客戶端一個 refreshToken,允許客戶端主動重新整理 jwt。一般而言,jwt 的過期時間可以設定為數小時,而 refreshToken 的過期時間設定為數天。我認為該方案並可行性是存在的,但是為了解決 jwt 的續簽把整個流程改變了,為什麼不考慮下 oauth2 的 password 模式和 client 模式呢? 
4.使用 redis 記錄獨立的過期時間 
  為了解決續簽問題,在 redis 中單獨給每個 jwt 設定了過期時間,每次訪問時重新整理 jwt 的過期時間,若 jwt 不存在於 redis 中則認為過期。 
同樣改變了 jwt 的流程,不過嘛,世間安得兩全法。我只能奉勸各位還未使用 jwt 做會話管理的朋友,儘量還是選用傳統的 session+cookie 方案,有很多成熟的分散式 session 框架和安全框架供你開箱即用。

 

參考文章:

1.https://blog.csdn.net/mn_kw/article/details/80522565

2.https://www.cnblogs.com/lonelyxmas/p/8024083.html

3.https://blog.csdn.net/qq_28165595/article/details/80214994