詳解 Json Web Token (如何為Flutter開發一個簡單的JWT解析庫)
JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS).
簡單解釋即:JWT是一個緊湊的、URL安全的、用於在雙方之間傳輸claims的這樣一個方法。而這個claims是用JSON格式編碼的,並且進行了數字簽名。
claim - 宣告;宣稱;斷言;(尤指對財產、土地等要求擁有的)所有權;(尤指向公司、政府等)索款,索賠。讓我們看一看具體的JWT長什麼樣子,就知道這個單詞用的其實是相當貼切的。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpZDEyMyIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjM5MDk5fQ.8HLeWIBn5d87r-XItgQJOnwqYGjJYrpKmz-2eC9fb8A
這個就是一個JWT的串,它分為3個部分,分別由兩個.
隔開,這三個部分分別是:
Header
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 // 是下面這個JSON object的base64編碼,它是JWT的頭資訊 // 表明了這個JWT用的是哪種簽名演算法,以及型別(JWT) { "alg": "HS256", "typ": "JWT" } 複製程式碼
Payload
eyJpc3MiOiJpZDEyMyIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjM5MDk5fQ // 是羨慕這個JSON object的base64編碼 { "iss": "id123", "name": "John Doe", "iat": 1516239022, "exp": 1516239099 } 複製程式碼
Payload裡放的其實就是claims(宣告),在使用中。JWT一般都是由伺服器返回給客戶端的,而客戶端每次請求時都會帶上這個JWT串,服務端通過解析這個Payload裡的內容,就知道這個請求來自於哪個使用者以及一些其他使用者基本資訊。
上面的這四個欄位iss
,name
,iat
,exp
,除了name
都是RFC7519定義的一些 Registered Claim Names, 可以認為標準的claim名字。iss
表示這個token是由誰簽發的,iat
token簽發的時間,exp
token過期時間。我們也可以像name
一樣,加一些自身邏輯需要的欄位。
那麼為什麼說claim這個單詞用的很貼切呢?首先,payload裡往往會是一些和身份認證、資源訪問許可權相關的內容;其次,payload欄位經過base64解碼之後,完全都是明文,任何人都可以修改,甚至偽造。也就是說任何人都可以通過偽造payload的內容來聲稱自己是誰、或者具有何種許可權,這都只是單方面的宣告,並不一定是有效/合法的。而要想知道是否合法,則需要第三個部分:
Signature
8HLeWIBn5d87r-XItgQJOnwqYGjJYrpKmz-2eC9fb8A // 計算HMAC SHA-256簽名值, 之後進行base64編碼 base64urlsafeencode( (HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret ) ) 複製程式碼
有了簽名,只需要對收到的JWT再進行一次簽名校驗就能知道這個JWT是不是合法的了。HS256(HMACSHA256)是jwt支援的簽名演算法之一,其他的詳見RFC7519
JSON in Flutter
接上文的Payload:
{ "iss": "id123", "name": "John Doe", "iat": 1516239022, "exp": 1516239099 } 複製程式碼
要解析這條資料,我們需要dart:convert為提供的jsonDecode
函式:
import 'dart:convert'; Map<String, dynamic> payload = jsonDecode(jsonString); print('${payload["iss"]} and ${payload["name"]}'); 複製程式碼
jsonDecode
函式簽名返回值是一個dynamic
型別,dynamic
可以是任意型別,字串,數字,列表等。很明顯,jsonDecode
需要能解析所有的合法JSON
型別,它的返回值可以是多種不同的型別。而在我們的例子裡,payload
是一個Map<String, dynamic>
,因為我們知道jsonString
是一個JSON
的物件型別。
json編碼也是同樣簡單:
var data = jsonEncode(payload); // data == '{"iss":"id123","name":"John Doe","iat":1516239022,"exp":1516239099}' 複製程式碼
除了以上手動方法之外,我們還可以用json_serializable來自動生成這些解析程式碼。
自動生成解析
首先你的pubspec.yaml需要一些額外的庫
dependencies: json_annotation: ^2.0.0 dev_dependencies: build_runner: ^1.0.0 json_serializable: ^2.0.0 複製程式碼
定義Claim資料類(claim.dart)
import 'package:json_annotation/json_annotation.dart'; // 檔案內容將會由工具自動生成 part 'claim.g.dart'; @JsonSerializable() class Claim { Claim(this.iss, this.name, this.iat, this.exp); String iss; String name; int iat; int exp; // 從json建立類物件的工廠函式,_$ClaimFromJson將會被定義在 claim.g.dart 檔案中 factory Claim.fromJson(Map<String, dynamic> json) => _$ClaimFromJson(json); // 將物件轉成json,_$ClaimToJson將會被定義在 claim.g.dart中 Map<String, dynamic> toJson() => _$ClaimToJson(this); } 複製程式碼
程式碼生成
執行flutter packages pub run build_runner build
。 Done,你現在可以像這樣使用Claim與json之間的轉換了:
// decode Map map = jsonDecode(jsonString); var claim = Claim.fromJson(map); // encode String json = jsonEncode(claim); 複製程式碼
生成JWT串
import 'dart:convert'; import 'package:crypto/crypto.dart'; var header = jsonEncode(<String, dynamic>{ "alg": "HS256", "typ": "JWT" }); Claim claim = Claim("id123", "John Doe",1516239022,1516239099); String payload = jsonEncode(claim); var headerBase64 = base64Url.encode(utf8.encode(header)); var claimBase64 = base64Url.encode(utf8.encode(payload)); var key = utf8.encode('secret'); var bytes = utf8.encode(headerBase64 + "." + claimBase64); var hmacSha256 = new Hmac(sha256, key); // HMAC-SHA256 var digest = hmacSha256.convert(bytes); var signature = base64Url.encode(digest.bytes); print("Just get a fresh new jwt: $headerBase64.$claimBase64.$signature"); 複製程式碼
我們把這寫功能稍微封裝一下,就是一個簡單的jwt編碼函式,可以處理任何@JsonSerializable()
註解的類例項。
String encodeJWT(dynamic obj) { var header = jsonEncode(<String, dynamic>{"alg": "HS256", "typ": "JWT"}); String payload = jsonEncode(obj); var headerBase64 = base64Url.encode(utf8.encode(header)); var claimBase64 = base64Url.encode(utf8.encode(payload)); var key = utf8.encode('secret'); var bytes = utf8.encode(headerBase64 + "." + claimBase64); var hmacSha256 = new Hmac(sha256, key); // HMAC-SHA256 var digest = hmacSha256.convert(bytes); var signature = base64Url.encode(digest.bytes); return "$headerBase64.$claimBase64.$signature"; } 複製程式碼