一個網站要辨識/驗證(authentication)訪客主要會使用session、cookie-based session或token這三種方式,JWT(JSON Web Token)顧名思義就是屬於token的驗證方式。使用JWT來驗證訪者,伺服器可以不用去記憶訪客的狀態,因為JWT本身就可以儲存少量的額外資料,這點和cookie-based session相像,但JWT不僅僅能放在Cookie中傳送,也還可以置於HTTP標頭中或是包含在URL內。那麼在Rocket框架中,要如何使用JWT來驗證訪客呢?



在開始進入Rocket框架之前,再來多介紹一下JWT吧!先從JWS(JSON Web Signature)開始講起。

JWS(JSON Web Signature)

JWS(JSON Web Signature)是一種基於base64url和JSON格式來傳送訊息的資料格式,它將訊息分為三個部份,分別是標頭(JOSE Header)、主體/負載(JWS Payload)和簽名(JWS Signature)。其中標頭的部份包含JWS Protected Header和JWS Unprotected Header兩種,這邊講的「Protected」有點模糊,筆者認為應該只是在產生簽名的時候要不要被納入計算中。一般來說我們都會使用JWS Protected Header,因為它會被納入簽名的計算,可避免標頭被竄改。

至於負載具體是什麼東東,JWS並未規範,它可以是任意的資料格式。

再來是簽名。JWS的簽名可以根據JWS Protected Header和負載產生出MAC(Message Authentication Code)。用來將簽名具體產生出來的演算法,其名稱必須被納入標頭中,因為驗證時可能需要被用到。如下:

{
    "alg": "<the algorithm used for generating the signature>"
}

另外JWS還有規定一些選填的標頭欄位,但是使用非巢狀的JWT + JWS時用不太到。

JWS還訂定了兩種序列化的方式,分別是JWS Compact Serialization和JWS JSON Serialization。由於JWT只使用JWS Compact Serialization,故在此就不介紹JWS JSON Serialization了。

JWS Compact Serialization的作法很簡單,就是將JWS的JWS Protected Header、負載和簽名這三個部份分別作base64url編碼,再用半形英文句點.串接在一起。如下:

base64url(JWS Protected Header).base64url(JWS Payload).base64url(JWS Signature)

JWT(JSON Web Token)

JWT是針對當JWS的負載或是當JWE的明碼是JSON物件時所做的規範,不過由於JWE並不太適合用在訪客驗證上,這邊就不多作介紹了。上面在介紹JWS時,提到了一些未具體規範的項目,在JWT這邊便開宗明義地規定了JWT的負載要是一個JSON物件,並且限定序列化的方式只能夠使用JWS Compact Serialization。

在JWT規範下,負載的部份稱為JWT Claims,一個「claim」就是一組key-value資料。以下是有被明確規範用途的欄位,都是選填的:

{
    "iss": "<the issure of this JWT>",
    "sub": "<the subject of this JWT>",
    "aud": ["<the recipients of this JWT>"],
    "exp": <the unix timestamp of expiration date/time in seconds>,
    "nbf": <the unix timestamp in seconds on which the JWT will start to be accepted for processing>,
    "iat": <the unix timestamp in seconds at which the JWT was issued>,
    "jti": <the unique id of this JWT>,
}

以上欄位中,最常使用的欄位應該是exp,它規定了這個JWT訊息的有效截止時間,拿來用作使用者登入驗證的話,就是登入失效的時間。

同樣地,用作使用者登入驗證的JWT + JWS,由於不需要讓客戶端自行校驗資料的完整性,因此可以直接使用HMAC + SHA256/384/512對稱密鑰演算法來產生簽名。一般在使用雜湊函數的時候會將要雜湊的資料進行加鹽(salt)的動作,避免被進行彩虹表攻擊(Rainbow Table Attack),然而加鹽可以有諸多種方式,安全性和效能容易有爭議。HMAC(Keyed-hash Message Authentication Code)是一種加鹽雜湊的標準函數,它將鹽作為密鑰(key),會先經過填充和雜湊處理後再與我們要雜湊的資料做串接,最後再進行一次雜湊。而利用JWT + JWS + HMAC + SHA256/384/512來進行使用者登入驗證時,通常不需要針對不同使用者設定不同的鹽,因為這裡並不存在使用者密碼會遭反解外流的可能,HMAC在這裡的用途純粹只是要利用只有伺服器知道的鹽來產生簽名而已。

使用者透過伺服器的登入API,提供登入所需的驗證資料(例如帳號、密碼)後,如果資料正確無誤,伺服器便可以利用JWT來儲存該使用者的ID和額外需要記錄的狀態,再將JWT序列化之後傳給使用者保存。之後使用者向伺服器發送請求時,便可以順便傳送這個經過序列化之後的JWT,來讓伺服器去重新使用相同的HMAC + SHA256/384/512去計算JWT的標頭和負載的簽名,看結果是否會與使用者傳來的JWT自帶的簽名一致,如果一致表示使用者傳來的JWT是沒有被竄改過的,就可以放心地拿來利用啦!

jwt-authorization Request Guard for Rocket Framework

「jwt-authorization Request Guard for Rocket Framework」是筆者開發的套件,可以利用程序式巨集來產生指定好密鑰以及簽名演算法的結構體,作為Rocket框架的請求守衛。

Crates.io

Cargo.toml

rocket-jwt-authorization = "*"

使用方法

由於「jwt-authorization Request Guard for Rocket Framework」是一個程序式巨集的函式庫,所以除了引用rocket_jwt_authorization至目前的Cargo專案外,還需要引用jwthmacsha2serdeserde_derive這5個crate。

jwt = "*"
hmac = "*"
sha2 = "*"
serde = "*"
serde_derive = "*"

rocket_jwt_authorization = "*"

以下是產生JWT請求守衛的程式:

#[macro_use]
extern crate rocket_jwt_authorization;

#[macro_use]
extern crate serde_derive;

#[derive(Debug, Serialize, Deserialize, JWT)]
#[jwt("secret_key", sha2::Sha256, Cookie = "access_token")]
pub struct UserAuth {
    #[serde(flatten)]
    registered: jwt::RegisteredClaims,
    id: i32,
}

以上的UserAuth結構體是一個JWT請求守衛,其使用的密鑰為secret_key,產生簽名的演算法為HS256(HMAC + SHA256),且會去讀取Cookie中的access_token欄位來反序列化JWT。

UserAuth結構體中的使用欄位可以是任意的。jwt::RegisteredClaims結構體提供了JWT有明確定義的欄位(如iss),可以搭配#[serde(flatten)]屬性將它們引用進我們自己的結構體當中。不過jwt::RegisteredClaims結構體中的欄位是沒有實質功能的,例如我們若設定了exp欄位,就必須要自行在路由處理程序中寫程式檢查這個JWT是否逾時。

如果想從Cookie之外來讀取請求中已序列化的JWT,例如要從標頭的Authorization欄位或是從網址的Query中讀取已序列化的JWT也是可行的,而且我們還可以自訂順序呢!如下:

#[derive(Debug, Serialize, Deserialize, JWT)]
#[jwt("secret_key", sha2::Sha256, Cookie = "access_token", Header, Query = "access_token")]
pub struct UserAuth {
    id: i32,
}

以上的UserAuth結構體作為JWT請求守衛使用時,會先去讀取請求Cookie中的access_token欄位。如果沒有發現JWT的存在,就會去讀取請求標頭的Authorization欄位,看有沒有以Bearer 為開頭的已序列化的JWT。如果還是沒有發現JWT的存在,就會去讀取Query中的access_token欄位。

如果不是當作請求守衛來使用,可以自行呼叫UserAuth結構體提供的verify_jwt_token關聯函數,並傳入已序列化的JWT來進行驗證和反序列的動作。

UserAuth結構實體提供的get_jwt_token方法則可以將該實體進行序列化。

另外,對於會去讀取Cookie的JWT請求守衛,結構實體還提供了set_cookieset_cookie_insecureremove_cookie方法,可以快速地對Cookie的指定欄位(在屬性中填入的Cookie欄位)進行操作。