1. 摘要
JSON Web Token(縮寫 JWT)是目前最流行的跨域認證解決方案,本文介紹它的原理,用法和詳細的數據結構。
2. JWT的定義
Json web token(JWT)是為了網絡應用環境間傳遞聲明而執行的一種基於JSON的開發標准(RFC 7519),該token被設計為緊湊且安全的,特別適用於分布式站點的單點登陸(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
什么情況下使用JWT比較適合? 授權:這是最常見的使用場景,解決單點登錄問題。因為JWT使用起來輕便,開銷小,服務端不用記錄用戶狀態信息(無狀態),所以使用比較廣泛; 信息交換:JWT是在各個服務之間安全傳輸信息的好方法。因為JWT可以簽名,例如,使用公鑰/私鑰對兒 - 可以確定請求方是合法的。此外,由於使用標頭和有效負載計算簽名,還可以驗證內容是否未被篡改。
3. JWT的原理和流程
3.1 跨域認證的問題
互聯網服務離不開用戶認證。一般流程是下面這樣:
1、用戶向服務器發送用戶名和密碼。 2、服務器驗證通過后,在當前對話(session)里面保存相關數據,比如用戶角色、登錄時間等等。 3、服務器向用戶返回一個 session_id,寫入用戶的 Cookie。 4、用戶隨后的每一次請求,都會通過 Cookie,將 session_id 傳回服務器。 5、服務器收到 session_id,找到前期保存的數據,由此得知用戶的身份。
這種模式的問題在於,擴展性(scaling)不好。單機當然沒有問題,如果是服務器集群,或者是跨域的服務導向架構,就要求 session 數據共享,每台服務器都能夠讀取 session。
舉例來說,A 網站和 B 網站是同一家公司的關聯服務。現在要求,用戶只要在其中一個網站登錄,再訪問另一個網站就會自動登錄,請問怎么實現?
一種解決方案是 session 數據持久化,寫入數據庫或別的持久層。各種服務收到請求后,都向持久層請求數據。這種方案的優點是架構清晰,缺點是工程量比較大。另外,持久層萬一掛了,就會單點失敗。 另一種方案是服務器索性不保存 session 數據了,所有數據都保存在客戶端,每次請求都發回服務器。JWT 就是這種方案的一個代表。
3.2 JWT 的原理
JWT 的原理是,服務器認證以后,生成一個 JSON 對象,發回給用戶,就像下面這樣。
{ "姓名": "張三", "角色": "管理員", "到期時間": "2018年7月1日0點0分" }
以后,用戶與服務端通信的時候,都要發回這個 JSON 對象。服務器完全只靠這個對象認定用戶身份。為了防止用戶篡改數據,服務器在生成這個對象的時候,會加上簽名。
服務器就不保存任何 session 數據了,也就是說,服務器變成無狀態了,從而比較容易實現擴展。
區別:
(1) session 存儲在服務端占用服務器資源,而 JWT 存儲在客戶端 (1) session 存儲在 Cookie 中,存在偽造跨站請求偽造攻擊的風險 (2) session 只存在一台服務器上,那么下次請求就必須請求這台服務器,不利於分布式應用 (3) 存儲在客戶端的 JWT 比存儲在服務端的 session 更具有擴展性
3.3 JWT的認證流程圖

流程說明: 1,瀏覽器發起請求登陸,攜帶用戶名和密碼; 2,服務端驗證身份,根據算法,將用戶標識符打包生成 token, 3,服務器返回JWT信息給瀏覽器,JWT不包含敏感信息; 4,瀏覽器發起請求獲取用戶資料,把剛剛拿到的 token一起發送給服務器; 5,服務器發現數據中有 token,驗明正身; 6,服務器返回該用戶的用戶資料;
3.4 JWT的6個優缺點
1、JWT默認不加密,但可以加密。生成原始令牌后,可以使用改令牌再次對其進行加密。 2、當JWT未加密方法是,一些私密數據無法通過JWT傳輸。 3、JWT不僅可用於認證,還可用於信息交換。善用JWT有助於減少服務器請求數據庫的次數。 4、JWT的最大缺點是服務器不保存會話狀態,所以在使用期間不可能取消令牌或更改令牌的權限。也就是說,一旦JWT簽發,在有效期內將會一直有效。 5、JWT本身包含認證信息,因此一旦信息泄露,任何人都可以獲得令牌的所有權限。為了減少盜用,JWT的有效期不宜設置太長。對於某些重要操作,用戶在使用時應該每次都進行進行身份驗證。 6、為了減少盜用和竊取,JWT不建議使用HTTP協議來傳輸代碼,而是使用加密的HTTPS協議進行傳輸。
4. JWT 的數據結構
4.1 JWT消息構成
一個token分3部分,按順序:
頭部(header)
載荷(payload)
簽證(signature)
- 對象為一個很長的字符串,字符之間通過"."分隔符分為三個子串。注意JWT對象為一個長字串,各字串之間也沒有換行符,一般格式為:xxxxx.yyyyy.zzzzz 。 例如 :
-
yJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

4.2 頭部(header)
JWT的頭部承載兩部分信息:
- 聲明類型,這里是jwt
- 聲明加密的算法 通常直接使用 HMAC SHA256
JWT里驗證和簽名使用的算法,可選擇下面的:
JWS |
算法名稱 |
描述 |
---|---|---|
HS256 |
HMAC256 |
HMAC with SHA-256 |
HS384 |
HMAC384 |
HMAC with SHA-384 |
HS512 |
HMAC512 |
HMAC with SHA-512 |
RS256 |
RSA256 |
RSASSA-PKCS1-v1_5 with SHA-256 |
RS384 |
RSA384 |
RSASSA-PKCS1-v1_5 with SHA-384 |
RS512 |
RSA512 |
RSASSA-PKCS1-v1_5 with SHA-512 |
ES256 |
ECDSA256 |
ECDSA with curve P-256 and SHA-256 |
ES384 |
ECDSA384 |
ECDSA with curve P-384 and SHA-384 |
ES512 |
ECDSA512 |
ECDSA with curve P-521 and SHA-512 |
JWT的頭部描述JWT元數據的JSON對象參考:
{ "alg": "HS256", "typ": "JWT" }
代碼樣例如下:
// header Map Map<String, Object> map = new HashMap<>(); map.put("alg", "HS256"); map.put("typ", "JWT");
4.3 載荷(payload)
Payload 部分也是一個 JSON 對象,用來存放實際需要傳遞的數據。JWT 規定了7個官方字段,供選用。
iss (issuer):簽發人
exp (expiration time):過期時間
sub (subject):主題
aud (audience):受眾
nbf (Not Before):生效時間
iat (Issued At):簽發時間
jti (JWT ID):編號
除以上默認字段外,我們還可以自定義私有字段,如下例:
{ "sub": "1234567890", "name": "chenyang", "admin": true }
注意,JWT 默認是不加密的,任何人都可以讀到,所以不要把秘密信息放在這個部分。 這個 JSON 對象也要使用 Base64URL 算法轉成字符串。 代碼樣例如下:
JWT.create().withHeader(map) // header .withClaim("iss", "Service") // payload .withClaim("aud", "APP") .withIssuedAt(iatDate) // sign time .withExpiresAt(expiresDate) // expire time .withClaim("name", "cy") // payload .withClaim("user_id", "11222");
4.4 簽名(signature)
Signature 部分是對前兩部分的簽名,防止數據篡改。 首先,需要指定一個密鑰(secret)。這個密鑰只有服務器才知道,不能泄露給用戶。然后,使用 Header 里面指定的簽名算法(默認是 HMAC SHA256),按照下面的公式產生簽名。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
算出簽名以后,把 Header、Payload、Signature 三個部分拼成一個字符串,每個部分之間用"點"(.)分隔,就構成整個JWT對象TOKEN, 就可以返回給用戶。
4.4.1 Base64URL算法
前面提到,Header 和 Payload 串型化的算法是 Base64URL。這個算法跟 Base64 算法基本類似,但有一些小的不同。 JWT 作為一個令牌(token),有些場合可能會放到 URL(比如 api.example.com/?token=xxx)。Base64 有三個字符+、/和=,在 URL 里面有特殊含義,所以要被替換掉:=被省略、+替換成-,/替換成_ 。這就是 Base64URL 算法。
4.5 JWT的用法
客戶端接收服務器返回的JWT,將其存儲在Cookie或localStorage中。 此后,客戶端將在與服務器交互中都會帶JWT。如果將它存儲在Cookie中,就可以自動發送,但是不會跨域,因此一般是將它放入HTTP請求的Header Authorization字段中。
Authorization: Bearer <token>
當跨域時,也可以將JWT被放置於POST請求的數據主體中。
5. JWT、JWS、JWE的區別
1)JWT(JSON Web Tokens),jwt長度較小,且可以使用URL傳輸(URL safe)。不想cookies只能在web環境起作用。 JWT可以同時使用在web環境和RESTfull的接口。
2)對於開發者來說,JWT與另外兩種相近的標准:JWS(JSON Web Signature)、JWE(JSON Web Encryption),容易引起混亂。
3)關於JWT的描述,可以參考RFC7519(https://tools.ietf.org/html/rfc7519)的描述: **JSON Web Token (JWT) **是一個間接地、URL安全的,表現為一組聲明,可以在雙方之間進行傳輸。一個JWT的聲明,是指經過編碼后的一個JSON對象,這個JSON對象可以是一個JSON Web Signature(JWS)結構的荷載(payload),或者是一個JSON Web Encryption(JWE)結構的明文。允許使用聲明進行數字簽名,或者通過一個Message Authentication Code(MAC)進行完整性保護可選擇是否加密。
簡單來說,JWTs表現為一組被編碼為JWS and/or JWE結構的JSON object的聲明(Claim).
換言之,一組JWT聲明(就是表現為JSON格式的Claims)被通過JWS結構或者JWE結構(或者同時使用兩種)發送,決定於你如何去實現它。所以,當你發送JWT給別人是,它實際上是一個JWT荷載或者JWE荷載。JWS荷載更加常用。
4)關於JWS 顧名思義,JWS模式對這個內容進行了數字化簽名。這個內容被用來存放JWT的聲明.服務端簽名出JWT並且發送到客戶端,並在用戶成功認證后進行應答。服務器期望客戶端在下次請求的時候將JWS作為請求的一部分,發送回服務端。
如果我們處理的客戶端是欺騙者怎么辦呢?這就是簽名(signature)需要出場的地方了。簽名攜帶了完整的可驗證的信息。換句話說,服務器可以確認,接收到的JWT聲明里的JWS是沒有經過欺騙客戶端、中間者進行修改的。 服務端通過驗證消息的簽名來確保客戶端沒有修改聲明。如果服務端檢測到任何修改,可以采取適當的動作(拒絕這次請求或者鎖定客戶端之類的)。 客戶端同樣可以驗證簽名,為了做到這點,客戶端也需要服務端的secret(密鑰)(如果這個JWT簽名是HMAC算法),或者需要服務端對公鑰(如果這個WJT是數字化簽名)。 特別注意:對於JWS,荷載(聲明部分)沒有進行加密,所以,不要發送任何敏感信息。
5)關於JWE JWE模式會對內容加密,而不是簽名。JWT的聲明會被加密。因此JWE帶來了保密性。JWE可以被簽名並附在JWS里。這樣的話就可以同時加密和簽名。因此得到了保密性(Confidentiality)、完整性(Integrity)、可認證(Authentication)。
6)那么對於客戶端,如何分辨JWS或者JWE呢? JWS的Header與JWE的Header是不同的,可以通過檢查“alg”Header參數的值來區分。如果這個值表現為一個數字簽名或者MAC的算法,或者是”none“,則它是一個JWS。 如果它表現為一個 Key Encryption, Key Wrapping, Direct Key Agreement, Key Agreement with Key Wrapping, or Direct Encryption algorithm。則它是一個JWE。 還可以通過Header里的“enc”(encryption algorithm)是否存在來判斷,如果"enc"這個成員存在的話說明是JWE,否則的話就是JWS.
JWT好文