03.04 初識JWT(Json Web Tokens) 認證一二三

來源:https://www.cnblogs.com/frank0812/p/11161573.html

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

<code>A由JWT頭部信息header加密得到
B由JWT用到的身份驗證信息json數據加密得到
C由A和B加密得到,是校驗部分/<code>

怎麼計算A?

<code>header格式為:
{
"typ": "JWT",
"alg": "HS256"
}/<code>

它就是一個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數據,是表明用戶身份的數據,可自行指定字段很靈活,也有固定字段表示特定含義(但不一定要包含特定字段,只是推薦)。一些字段的含義:

<code> {
     "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"] // 自定義字段
  }/<code>

自定義字段的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對象中,並重定向到首頁,完成登錄過程;
初識JWT(Json Web Tokens) 認證一二三

2.請求認證

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

初識JWT(Json Web Tokens) 認證一二三

  • 客戶端(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(Json Web Tokens) 認證一二三

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

<code> 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> 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
}/<string>/<code>

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

<code>static void Main(string[] args)
{
//服務端的秘鑰,一般放在配置文件中
const string secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
//負荷(payload)
var payload = new Dictionary<string>
{
{ "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);
}
}/<string>/<code>

運行結果如圖所示:

初識JWT(Json Web Tokens) 認證一二三

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




分享到:


相關文章: